diff options
author | usodhi <61431818+usodhi@users.noreply.github.com> | 2020-11-17 19:28:30 +0530 |
---|---|---|
committer | usodhi <61431818+usodhi@users.noreply.github.com> | 2020-11-17 19:28:30 +0530 |
commit | 86408e6d93fbe6501694371736fe74b81ed39cf3 (patch) | |
tree | 17b89a6209c66284f89e2636a8157435ce1045c0 | |
parent | a002e0e5c5910f78c8f3910ad4101386d30ebf70 (diff) | |
parent | 28dccafaa4aa446dd88c1b6f4218a0d7f79fa1bb (diff) |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into acls_uv
96 files changed, 2087 insertions, 1443 deletions
diff --git a/backup.bat b/backup.bat new file mode 100644 index 000000000..5a51ace4d --- /dev/null +++ b/backup.bat @@ -0,0 +1,9 @@ +@echo on +for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a" +set "YY=%dt:~2,2%" & set "YYYY=%dt:~0,4%" & set "MM=%dt:~4,2%" & set "DD=%dt:~6,2%" +set "HH=%dt:~8,2%" & set "Min=%dt:~10,2%" & set "Sec=%dt:~12,2%" + +set "datestamp=%YYYY%%MM%%DD%" & set "timestamp=%HH%%Min%%Sec%" +set "fullstamp=%YYYY%-%MM%-%DD%_%HH%-%Min%-%Sec%" +mkdir "C:\Users\%USERNAME%\Desktop\backups\%fullstamp%" +"C:\Program Files\MongoDB\Server\4.2\bin\mongodump.exe" --db Dash --gzip --out "C:\Users\%USERNAME%\Desktop\backups\%fullstamp%" diff --git a/package-lock.json b/package-lock.json index 2cc50060f..42cbc1fd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -585,7 +585,6 @@ "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", - "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -639,7 +638,6 @@ "version": "3.4.33", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", - "dev": true, "requires": { "@types/node": "*" } @@ -672,6 +670,14 @@ "@types/keygrip": "*" } }, + "@types/cors": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz", + "integrity": "sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w==", + "requires": { + "@types/express": "*" + } + }, "@types/dotenv": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz", @@ -709,7 +715,6 @@ "version": "4.17.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", - "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", @@ -731,7 +736,6 @@ "version": "4.17.5", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz", "integrity": "sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==", - "dev": true, "requires": { "@types/node": "*", "@types/range-parser": "*" @@ -847,8 +851,7 @@ "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", - "dev": true + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" }, "@types/minimatch": { "version": "3.0.3", @@ -1105,14 +1108,12 @@ "@types/qs": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", - "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==", - "dev": true + "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" }, "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", - "dev": true + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, "@types/rc-switch": { "version": "1.9.0", @@ -1266,7 +1267,6 @@ "version": "1.13.3", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", - "dev": true, "requires": { "@types/express-serve-static-core": "*", "@types/mime": "*" diff --git a/package.json b/package.json index 6736d27f6..13849c0f3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts", "oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug -- src/server/index.ts", "debug": "cross-env NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts", + "monitor": "cross-env MONITORED=true NODE_OPTIONS=--max_old_space_size=4096 ts-node src/server/index.ts", "build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack --env production", "test": "mocha -r ts-node/register test/**/*.ts", "tsc": "tsc" @@ -124,6 +125,7 @@ "@hig/theme-context": "^2.1.3", "@hig/theme-data": "^2.16.1", "@material-ui/core": "^4.11.0", + "@types/cors": "^2.8.8", "@types/google-maps": "^3.2.2", "@types/reveal": "^3.3.33", "@types/webscopeio__react-textarea-autocomplete": "^4.6.1", diff --git a/session.config.json b/session.config.json index 5440d9bbd..f613dd904 100644 --- a/session.config.json +++ b/session.config.json @@ -1,7 +1,7 @@ { "showServerOutput": false, "ports": { - "server": 443, + "server": 1050, "socket": 4321 }, "polling": { diff --git a/src/Utils.ts b/src/Utils.ts index cc7ee9537..daacca51d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -475,7 +475,7 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; }; -export function smoothScroll(duration: number, element: HTMLElement, to: number) { +export function smoothScroll(duration: number, element: HTMLElement, to: number, finish?: () => void) { const start = element.scrollTop; const change = to - start; const startDate = new Date().getTime(); @@ -489,6 +489,7 @@ export function smoothScroll(duration: number, element: HTMLElement, to: number) requestAnimationFrame(animateScroll); } else { element.scrollTop = to; + finish?.(); } }; animateScroll(); diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 00f9877c3..1d7497cf8 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -28,13 +28,13 @@ import * as rp from 'request-promise'; export namespace DocServer { let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {}; - export function PRINT_CACHE() { + export function UPDATE_SERVER_CACHE(print: boolean = false) { const strings: string[] = []; Array.from(Object.keys(_cache)).forEach(key => { const doc = _cache[key]; if (doc instanceof Doc) strings.push(StrCast(doc.author) + " " + StrCast(doc.title) + " " + StrCast(Doc.GetT(doc, "title", "string", true))); }); - strings.sort().forEach((str, i) => console.log(i.toString() + " " + str)); + print && strings.sort().forEach((str, i) => console.log(i.toString() + " " + str)); rp.post(Utils.prepend("/setCacheDocumentIds"), { body: { cacheDocumentIds: Array.from(Object.keys(_cache)).join(";"), @@ -348,7 +348,6 @@ export namespace DocServer { } if (requestedIds.length) { - // 2) synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string) // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of // the fields have been returned from the server diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 45c465d84..2d8a897a5 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -61,6 +61,7 @@ export interface DocumentOptions { _autoHeight?: boolean; _headerHeight?: number; // height of header of custom notes _headerFontSize?: number; // font size of header of custom notes + _headerPointerEvents?: string; // types of events the header of a custom text document can consume _panX?: number; _panY?: number; _width?: number; @@ -236,6 +237,8 @@ class EmptyBox { export namespace Docs { + export let newAccount: boolean = false; + export namespace Prototypes { type LayoutSource = { LayoutString: (key: string) => string }; @@ -392,7 +395,7 @@ export namespace Docs { // non-guid string ids for each document prototype const prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix); // fetch the actual prototype documents from the server - const actualProtos = await DocServer.GetRefFields(prototypeIds); + const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds); // update this object to include any default values: DocumentOptions for all prototypes prototypeIds.map(id => { @@ -1099,7 +1102,7 @@ export namespace DocUtils { }); } ctor = Docs.Create.WebDocument; - options = { ...options, _fitWidth: true, _nativeWidth: 850, _width: 400, _height: 512, title: path, }; + options = { ...options, _fitWidth: true, _width: 400, _height: 512, title: path, }; } return ctor ? ctor(path, options) : undefined; } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index dcbeba8cd..4f054269f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -405,7 +405,7 @@ export class CurrentUserUtils { selection: { type: "text", anchor: 1, head: 1 }, storedMarks: [] }; - const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "header", version: headerViewVersion, target: doc, _height: 70, _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) }, "header"); // text needs to be a space to allow templateText to be created + const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "header", version: headerViewVersion, target: doc, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) }, "header"); // text needs to be a space to allow templateText to be created headerTemplate[DataSym].layout = "<div style={'height:100%'}>" + " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" + @@ -878,7 +878,7 @@ export class CurrentUserUtils { // Sharing sidebar is where shared documents are contained static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { if (doc.myLinkDatabase === undefined) { - let linkDocs = await DocServer.GetRefField(linkDatabaseId); + let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId); if (!linkDocs) { linkDocs = new Doc(linkDatabaseId, true); (linkDocs as Doc).author = Doc.CurrentUserEmail; @@ -888,7 +888,7 @@ export class CurrentUserUtils { doc.myLinkDatabase = new PrefetchProxy(linkDocs); } if (doc.mySharedDocs === undefined) { - let sharedDocs = await DocServer.GetRefField(sharingDocumentId + "outer"); + let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer"); if (!sharedDocs) { sharedDocs = Docs.Create.StackingDocument([], { title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, @@ -1024,6 +1024,7 @@ export class CurrentUserUtils { // Doc.AddDocToList(Cast(doc["template-notes"], Doc, null), "data", deleg); // } // }); + setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500); return doc; } @@ -1047,8 +1048,12 @@ export class CurrentUserUtils { await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => { const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids); if (userDocumentId !== "guest") { - return DocServer.GetRefField(userDocumentId).then(async field => - this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId, linkDatabaseId)); + return DocServer.GetRefField(userDocumentId).then(async field => { + Docs.newAccount = !(field instanceof Doc); + await Docs.Prototypes.initialize(); + const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc; + return this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId); + }); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); } @@ -1108,7 +1113,7 @@ export class CurrentUserUtils { const response = await fetch(upload, { method: "POST", body: formData }); const json = await response.json(); if (json !== "error") { - const doc = await DocServer.GetRefField(json); + const doc = Docs.newAccount ? undefined : await DocServer.GetRefField(json); if (doc instanceof Doc) { setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added. @@ -1119,6 +1124,9 @@ export class CurrentUserUtils { const disposer = OverlayView.ShowSpinner(); DocListCastAsync(importDocs.data).then(async list => { const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {}); + if (results.length !== input.files?.length) { + alert("Error uploading files - possibly due to unsupported file types"); + } list?.splice(0, 0, ...results); disposer(); }); @@ -1185,6 +1193,7 @@ export class CurrentUserUtils { public static get MyRecentlyClosed() { return Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc, null); } public static get MyDashboards() { return Cast(Doc.UserDoc().myDashboards, Doc, null); } public static get EmptyPane() { return Cast(Doc.UserDoc().emptyPane, Doc, null); } + public static get OverlayDocs() { return DocListCast((Doc.UserDoc().myOverlayDocs as Doc)?.data); } } Scripting.addGlobal(function openDragFactory(dragFactory: Doc) { @@ -1203,7 +1212,5 @@ Scripting.addGlobal(function createNewPresentation() { return MainView.Instance. "creates a new presentation when called"); Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)"); -Scripting.addGlobal(function directLinks(doc: any) { return new List(LinkManager.Instance.getAllDirectLinks(doc)); }, - "returns all the links directly to the document", "(doc: any)"); Scripting.addGlobal(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index dc911ea75..a6816c7f9 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -110,7 +110,7 @@ export class DocumentManager { public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { const views = this.getDocumentViews(toFind).filter(view => view.props.Document !== originatingDoc); - return views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); + return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); } public getDocumentViews(toFind: Doc): DocumentView[] { const toReturn: DocumentView[] = []; @@ -128,19 +128,19 @@ export class DocumentManager { } - static addRightSplit = (doc: Doc, finished?: () => void) => { + static addView = (doc: Doc, finished?: () => void) => { CollectionDockingView.AddSplit(doc, "right"); finished?.(); } public jumpToDocument = async ( targetDoc: Doc, // document to display willZoom: boolean, // whether to zoom doc to take up most of screen - createViewFunc = DocumentManager.addRightSplit, // how to create a view of the doc if it doesn't exist + createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist docContext?: Doc, // context to load that should contain the target linkDoc?: Doc, // link that's being followed closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc - finished?: () => void + finished?: () => void, ): Promise<void> => { const getFirstDocView = DocumentManager.Instance.getFirstDocumentView; const focusAndFinish = () => { finished?.(); return false; }; @@ -163,21 +163,19 @@ export class DocumentManager { if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? const sameContext = annotatedDoc && annotatedDoc === originatingDoc?.context; if (originatingDoc?.isPushpin) { - const hide = !docView.props.Document.hidden; - docView.props.focus(docView.props.Document, willZoom, undefined, (notfocused: boolean) => { // bcz: Argh! TODO: Need to add a notFocused argument to the after finish callback function that indicates whether the window had to scroll to show the target - if (notfocused || docView.props.Document.hidden) { + docView.props.focus(docView.props.Document, willZoom, undefined, (didFocus: boolean) => { + if (!didFocus || docView.props.Document.hidden) { docView.props.Document.hidden = !docView.props.Document.hidden; } return focusAndFinish(); - // @ts-ignore bcz: Argh TODO: Need to add a parameter to focus() everywhere for whether focus should center the target's container in the view or not. // here we don't want to focus the container if the source and target are in the same container - }, sameContext); + }, sameContext, false);// don't want to focus the container if the source and target are in the same container, so pass 'sameContext' for dontCenter parameter //finished?.(); } else { docView.select(false); docView.props.Document.hidden && (docView.props.Document.hidden = undefined); // @ts-ignore - docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish, sameContext); + docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish, sameContext, false); } highlight(); } else { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 3a0f306f3..86e2d339e 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -201,7 +201,14 @@ export namespace DragManager { } // drag a document and drop it (or make an alias/copy on drop) - export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + export function StartDocumentDrag( + eles: HTMLElement[], + dragData: DocumentDragData, + downX: number, + downY: number, + options?: DragOptions, + dropEvent?: () => any + ) { const addAudioTag = (dropDoc: any) => { dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField); dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(dropDoc); @@ -209,6 +216,7 @@ export namespace DragManager { }; const finishDrag = (e: DragCompleteEvent) => { const docDragData = e.docDragData; + if (dropEvent) dropEvent(); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; docDragData.droppedDocuments = @@ -408,7 +416,13 @@ export namespace DragManager { }); const hideSource = options?.hideSource ? true : false; - eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = hideSource) : (ele.hidden = hideSource)); + eles.forEach(ele => { + if (ele.parentElement && ele.parentElement?.className === dragData.dragDivName) { + ele.parentElement.hidden = hideSource; + } else { + ele.hidden = hideSource; + } + }); SnappingManager.SetIsDragging(true); let lastX = downX; @@ -506,27 +520,25 @@ export namespace DragManager { const hideDragShowOriginalElements = () => { dragLabel.style.display = "none"; dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); - eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (ele.hidden = false)); + eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.hidden = ele.parentElement.hidden = false) : (ele.hidden = false)); }; const endDrag = action(() => { + hideDragShowOriginalElements(); document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); + SnappingManager.SetIsDragging(false); SnappingManager.clearSnapLines(); batch.end(); }); AbortDrag = () => { - hideDragShowOriginalElements(); - SnappingManager.SetIsDragging(false); options?.dragComplete?.(new DragCompleteEvent(true, dragData)); endDrag(); }; const upHandler = (e: PointerEvent) => { - hideDragShowOriginalElements(); dispatchDrag(eles, e, dragData, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag); - SnappingManager.SetIsDragging(false); - endDrag(); options?.dragComplete?.(new DragCompleteEvent(false, dragData)); + endDrag(); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 0d12b39b8..9bd92a316 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -1,7 +1,6 @@ import { Doc } from "../../../fields/Doc"; import { ImageField } from "../../../fields/URLField"; -import { Cast, StrCast } from "../../../fields/Types"; -import { Docs } from "../../documents/Documents"; +import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { Networking } from "../../Network"; import { Id } from "../../../fields/FieldSymbols"; import { Utils } from "../../../Utils"; @@ -22,6 +21,7 @@ export namespace ImageUtils { } = await Networking.PostToServer("/inspectImage", { source }); document.exif = error || Doc.Get.FromJson({ data }); const proto = Doc.GetProto(document); + nativeWidth && (document._height = NumCast(document._width) * nativeHeight / nativeWidth); proto["data-nativeWidth"] = nativeWidth; proto["data-nativeHeight"] = nativeHeight; proto["data-path"] = source; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 1ba6cff6d..802b8ae7b 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -22,119 +22,58 @@ import { computedFn } from "mobx-utils"; export class LinkManager { private static _instance: LinkManager; - public static currentLink: Opt<Doc>; + public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); } - public static get Instance(): LinkManager { - return this._instance || (this._instance = new this()); - } - - private constructor() { - } + public addLink(linkDoc: Doc) { return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); } + public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } + public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } + public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor + public getAllDirectLinks(anchor: Doc): Doc[] { return this.directLinker(anchor); } // finds all links that contain the given anchor + public getAllLinks(): Doc[] { return this.allLinks(); } - public getAllLinks(): Doc[] { + allLinks = computedFn(function allLinks(this: any): Doc[] { const lset = new Set<Doc>(DocListCast(Doc.LinkDBDoc().data)); - SharingManager.Instance.users.forEach(user => { - DocListCast(user.linkDatabase?.data).map(doc => { - lset.add(doc); - }); - }); + SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); return Array.from(lset); - } - - public addLink(linkDoc: Doc): boolean { - return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); - } - - public deleteLink(linkDoc: Doc): boolean { - return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); - } + }, true); - // finds all links that contain the given anchor - public getAllDirectLinks(anchor: Doc): Doc[] { - const related = LinkManager.Instance.getAllLinks().filter(link => link).filter(link => { - const a1 = Cast(link.anchor1, Doc, null); - const a2 = Cast(link.anchor2, Doc, null); - const protomatch1 = Doc.AreProtosEqual(anchor, a1); - const protomatch2 = Doc.AreProtosEqual(anchor, a2); - return ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor)); + directLinker = computedFn(function directLinker(this: any, anchor: Doc): Doc[] { + return LinkManager.Instance.allLinks().filter(link => { + const a1 = Cast(link?.anchor1, Doc, null); + const a2 = Cast(link?.anchor2, Doc, null); + return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor)); }); - return related; - } + }, true); - relatedLinker = computedFn(function realtedLinker(this: any, anchor: Doc) { - const related = LinkManager.Instance.getAllDirectLinks(anchor); - DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).map(anno => { - related.push(...LinkManager.Instance.getAllRelatedLinks(anno)); - }); - return related; - }.bind(this), true); - - // finds all links that contain the given anchor - public getAllRelatedLinks(anchor: Doc): Doc[] { - return this.relatedLinker(anchor); - } - - public deleteAllLinksOnAnchor(anchor: Doc) { - const related = LinkManager.Instance.getAllRelatedLinks(anchor); - related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); - } - - // gets the groups associates with an anchor in a link - public getAnchorGroups(linkDoc: Doc, anchor?: Doc): Array<Doc> { - if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { - return DocListCast(linkDoc.anchor1Groups); - } else { - return DocListCast(linkDoc.anchor2Groups); - } - } - public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) { - Doc.GetProto(linkDoc).linkRelationship = groupDoc.linkRelationship; - } - - // removes group doc of given group type only from given anchor on given link - public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) { - Doc.GetProto(linkDoc).linkRelationship = "-ungrouped-"; - } + relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { + return DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).reduce((list, anno) => + [...list, ...LinkManager.Instance.relatedLinker(anno)], + LinkManager.Instance.directLinker(anchor).slice()); + }, true); // returns map of group type to anchor's links in that group type public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> { - const related = this.getAllRelatedLinks(anchor); const anchorGroups = new Map<string, Array<Doc>>(); - related.forEach(link => { + this.relatedLinker(anchor).forEach(link => { if (!link.linkRelationship || link?.linkRelationship !== "-ungrouped-") { const group = anchorGroups.get(StrCast(link.linkRelationship)); anchorGroups.set(StrCast(link.linkRelationship), group ? [...group, link] : [link]); - } else { // if link is in no groups then put it in default group const group = anchorGroups.get("*"); anchorGroups.set("*", group ? [...group, link] : [link]); } - }); return anchorGroups; } - // returns a list of all metadata docs associated with the given group type - public getAllMetadataDocsInGroup(groupType: string): Array<Doc> { - const md: Doc[] = []; - const allLinks = LinkManager.Instance.getAllLinks(); - allLinks.forEach(linkDoc => { - if (StrCast(linkDoc.linkRelationship).toUpperCase() === groupType.toUpperCase()) { md.push(linkDoc); } - }); - return md; - } - // checks if a link with the given anchors exists public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean { - const allLinks = LinkManager.Instance.getAllLinks(); - const index = allLinks.findIndex(linkDoc => { - return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) || - (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1)); - }); - return index !== -1; + return -1 !== LinkManager.Instance.allLinks().findIndex(linkDoc => + (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) || + (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1))); } // finds the opposite anchor of a given anchor in a link diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 0f7ad6d0a..569ad8ab4 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -76,7 +76,7 @@ export namespace UndoManager { export let undoStack: UndoBatch[] = observable([]); export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; - let batchCounter = 0; + export let batchCounter = 0; let undoing = false; let tempEvents: UndoEvent[] | undefined = undefined; diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 7467bc043..b514de5f2 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -43,7 +43,6 @@ .contextMenu-item { // width: 11vw; //10vw height: 25px; //2vh - background: whitesmoke; display: flex; //comment out to allow search icon to be inline with search text justify-content: left; align-items: center; @@ -58,7 +57,6 @@ // padding: 10px 0px 10px 0px; white-space: nowrap; font-size: 13px; - color: grey; letter-spacing: 2px; text-transform: uppercase; padding-right: 30px; @@ -75,7 +73,6 @@ .contextMenu-description { // width: 11vw; //10vw - background: whitesmoke; display: flex; //comment out to allow search icon to be inline with search text justify-content: left; -webkit-touch-callout: none; @@ -89,7 +86,6 @@ // padding: 10px 0px 10px 0px; white-space: nowrap; font-size: 10px; - color: grey; letter-spacing: 1px; text-transform: uppercase; padding-right: 30px; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 076502042..fa0b9a238 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -26,6 +26,7 @@ import { TemplateMenu } from "./TemplateMenu"; import React = require("react"); import { PresBox } from './nodes/PresBox'; import { undoBatch } from '../util/UndoManager'; +import { CollectionViewType } from './collections/CollectionView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -186,7 +187,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get pinButton() { const targetDoc = this.view0?.props.Document; const isPinned = targetDoc && Doc.isDocPinned(targetDoc); - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation"}</div></>}> + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{SelectionManager.SelectedDocuments().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}</div></>}> <div className="documentButtonBar-linker" style={{ color: "white" }} onClick={undoBatch(e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, false)))}> @@ -194,25 +195,44 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </div></Tooltip>; } + @undoBatch + @action + pinWithView = (targetDoc: Doc) => { + if (targetDoc) { + TabDocView.PinDoc(targetDoc, false); + const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; + const scrollable: boolean = (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || 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.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 = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 17, transform: 'translate(0, 1px)' }} />; const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with current view"}</div></>}> - <div className="documentButtonBar-linker" - onClick={undoBatch(e => { - if (targetDoc) { - TabDocView.PinDoc(targetDoc, false); - const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeDoc.presPinView = true; - activeDoc.presPinViewX = x; - activeDoc.presPinViewY = y; - activeDoc.presPinViewScale = scale; - } - })}> + <div + className="documentButtonBar-linker" + onClick={() => this.pinWithView(targetDoc)}> {presPinWithViewIcon} </div> </Tooltip>; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 66f0dd99d..63b99cd85 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -177,19 +177,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const selectedDocs = SelectionManager.SelectedDocuments(); if (selectedDocs.length) { if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key - const alias = Doc.MakeAlias(selectedDocs[0].props.Document); - alias.context = undefined; - //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0]); - CollectionDockingView.AddSplit(alias, "right"); + selectedDocs[0].props.Document._fullScreenView = Doc.MakeAlias(selectedDocs[0].props.Document); + (selectedDocs[0].props.Document._fullScreenView as Doc).context = undefined; + CollectionDockingView.AddSplit(selectedDocs[0].props.Document._fullScreenView as Doc, "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.Instance?.OpenFullScreen(selectedDocs[0]); CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); } else { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); + CollectionDockingView.ToggleSplit(Cast(selectedDocs[0].props.Document._fullScreenView, Doc, null) || selectedDocs[0].props.Document, "right"); } } } @@ -327,8 +325,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height) { this._inkDocs.push({ x: doc.x, y: doc.y, width: doc._width, height: doc._height }); if (InkStrokeProperties.Instance?._lock) { - doc._nativeHeight = doc._height; - doc._nativeWidth = doc._width; + Doc.SetNativeHeight(doc, doc._height); + Doc.SetNativeWidth(doc, doc._width); } } })); @@ -356,11 +354,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.SelectedDocuments()[0]; let thisPt = { thisX: e.clientX - this._offX, thisY: e.clientY - this._offY }; - var fixedAspect = first.layoutDoc._nativeWidth ? NumCast(first.layoutDoc._nativeWidth) / NumCast(first.layoutDoc._nativeHeight) : 0; + var fixedAspect = Doc.NativeAspect(first.layoutDoc); SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { const doc = Document(element.rootDoc); if (doc.type === DocumentType.INK && doc._width && doc._height && InkStrokeProperties.Instance?._lock) { - fixedAspect = NumCast(doc._nativeWidth) / NumCast(doc._nativeHeight); + fixedAspect = Doc.NativeHeight(doc); } })); @@ -394,7 +392,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ((element.rootDoc.type === DocumentType.RTF || element.rootDoc.type === DocumentType.COMPARISON || (element.rootDoc.type === DocumentType.WEB && Doc.LayoutField(element.rootDoc) instanceof HtmlField)) - && element.layoutDoc._nativeHeight) && element.toggleNativeDimensions())); + && Doc.NativeHeight(element.layoutDoc)) && element.toggleNativeDimensions())); switch (this._resizeHdlId) { case "": break; case "documentDecorations-topLeftResizer": @@ -440,11 +438,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } SelectionManager.SelectedDocuments().forEach(action((docView: DocumentView) => { - if (e.ctrlKey && !docView.props.Document._nativeHeight) docView.toggleNativeDimensions(); + if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = Document(docView.rootDoc); - let nwidth = returnVal(docView.NativeWidth?.(), doc._nativeWidth); - let nheight = returnVal(docView.NativeHeight?.(), doc._nativeHeight); + let nwidth = returnVal(docView.NativeWidth?.(), Doc.NativeWidth(doc)); + let nheight = returnVal(docView.NativeHeight?.(), Doc.NativeHeight(doc)); const width = (doc._width || 0); let height = (doc._height || (nheight / nwidth * width)); height = !height || isNaN(height) ? 20 : height; @@ -463,26 +461,23 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc.x = (doc.x || 0) + dX * (actualdW - width); doc.y = (doc.y || 0) + dY * (actualdH - height); const fixedAspect = (nwidth && nheight); - const fieldKey = docView.LayoutFieldKey; if (fixedAspect && (!nwidth || !nheight)) { - doc[DataSym][fieldKey + "-nativeWidth"] = doc._nativeWidth = nwidth = doc._width || 0; - doc[DataSym][fieldKey + "-nativeHeight"] = doc._nativeHeight = nheight = doc._height || 0; + doc._nativeWidth = nwidth = doc._width || 0; + doc._nativeHeight = nheight = doc._height || 0; } - const anno = Cast(doc.annotationOn, Doc, null); - if (e.ctrlKey && (anno || doc.type === DocumentType.IMG)) { + if (e.ctrlKey && [DocumentType.IMG, DocumentType.SCREENSHOT, DocumentType.VID].includes(doc.type as DocumentType)) { dW !== 0 && runInAction(() => { - const dataDoc = (anno ?? doc)[DataSym]; - const annoFieldKey = Doc.LayoutFieldKey(anno ?? doc); - const nw = NumCast(dataDoc[annoFieldKey + "-nativeWidth"]); - const nh = NumCast(dataDoc[annoFieldKey + "-nativeHeight"]); - dataDoc[annoFieldKey + "-nativeWidth"] = nw + (dW > 0 ? 10 : -10); - dataDoc[annoFieldKey + "-nativeHeight"] = nh + (dW > 0 ? 10 : -10) * nh / nw; + const dataDoc = doc[DataSym]; + const nw = Doc.NativeWidth(dataDoc); + const nh = Doc.NativeHeight(dataDoc); + Doc.SetNativeWidth(dataDoc, nw + (dW > 0 ? 10 : -10)); + Doc.SetNativeHeight(dataDoc, nh + (dW > 0 ? 10 : -10) * nh / nw); }); } else if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH) || dragRight) { if (!fixedAspect || (dragRight && e.ctrlKey)) { - doc[DataSym][fieldKey + "-nativeWidth"] = doc._nativeWidth = actualdW / (doc._width || 1) * (doc._nativeWidth || 0); + doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc); } doc._width = actualdW; if (fixedAspect && !doc._fitWidth) doc._height = nheight / nwidth * doc._width; @@ -490,7 +485,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } else { if (!fixedAspect || (dragBottom && (e.ctrlKey || docView.layoutDoc._fitWidth))) { - doc[DataSym][fieldKey + "-nativeHeight"] = doc._nativeHeight = actualdH / (doc._height || 1) * (doc._nativeHeight || 0); + doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc); } doc._height = actualdH; if (fixedAspect && !doc._fitWidth) doc._width = nwidth / nheight * doc._height; diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index 19512362e..4a89cc69c 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -4,6 +4,7 @@ word-wrap: break-word; hyphens: auto; overflow: hidden; + min-width: 20; } .editableView-container-editing-oneLine { diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 8b1b12365..ed7a8265f 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -35,6 +35,7 @@ export interface EditableProps { sizeToContent?: boolean; maxHeight?: number; display?: string; + overflow?: string; autosuggestProps?: { resetValue: () => void; value: string, @@ -82,11 +83,24 @@ export class EditableView extends React.Component<EditableProps> { // } @action + componentDidUpdate() { + if (this._editing && this.props.editing === false) { + this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); + } else if (this.props.editing !== undefined) { + this._editing = this.props.editing; + } + } + + @action componentDidMount() { if (this._ref.current && this.props.onDrop) { DragManager.MakeDropTarget(this._ref.current, this.props.onDrop.bind(this)); } } + @action + componentWillUnmount() { + this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); + } _didShow = false; @@ -131,14 +145,16 @@ export class EditableView extends React.Component<EditableProps> { @action onClick = (e: React.MouseEvent) => { - e.nativeEvent.stopPropagation(); - if (this._ref.current && this.props.showMenuOnLoad) { - this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y); - } else if (!this.props.onClick?.(e)) { - this._editing = true; - this.props.isEditingCallback?.(true); + if (this.props.editing !== false) { + e.nativeEvent.stopPropagation(); + if (this._ref.current && this.props.showMenuOnLoad) { + this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y); + } else if (!this.props.onClick?.(e)) { + this._editing = true; + this.props.isEditingCallback?.(true); + } + e.stopPropagation(); } - e.stopPropagation(); } @action @@ -168,6 +184,7 @@ export class EditableView extends React.Component<EditableProps> { } _ref = React.createRef<HTMLDivElement>(); + _inputref = React.createRef<HTMLInputElement>(); renderEditor() { return this.props.autosuggestProps ? <Autosuggest @@ -185,7 +202,7 @@ export class EditableView extends React.Component<EditableProps> { onChange: this.props.autosuggestProps.onChange }} /> - : <input className="editableView-input" + : <input className="editableView-input" ref={this._inputref} defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus={true} @@ -211,9 +228,9 @@ export class EditableView extends React.Component<EditableProps> { setTimeout(() => this.props.autosuggestProps?.resetValue(), 0); return this.props.contents instanceof ObjectField ? (null) : <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} - style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: this.props.height || "auto", maxHeight: this.props.maxHeight }} + style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: "17px", whiteSpace: "nowrap", height: this.props.height || "auto", maxHeight: this.props.maxHeight }} onClick={this.onClick} placeholder={this.props.placeholder}> - <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{ + <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }} >{ this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} </span> </div>; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 18743e850..ffa089af1 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -655,7 +655,7 @@ export class GestureOverlay extends Touchable { this._points = []; } //get out of ink mode after each stroke= - if (!CollectionFreeFormViewChrome.Instance._keepMode) { + if (CollectionFreeFormViewChrome.Instance && !CollectionFreeFormViewChrome.Instance?._keepMode) { Doc.SetSelectedTool(InkTool.None); CollectionFreeFormViewChrome.Instance._selected = CollectionFreeFormViewChrome.Instance._shapesNum; SetActiveArrowStart("none"); @@ -839,14 +839,15 @@ export class GestureOverlay extends Touchable { ) || false; } - getBounds = (stroke: InkData) => { - const xs = stroke.map(p => p.X); - const ys = stroke.map(p => p.Y); + getBounds = (stroke: InkData, pad?: boolean) => { + const padding = pad ? [-20000, 20000] : []; + const xs = [...padding, ...stroke.map(p => p.X)]; + const ys = [...padding, ...stroke.map(p => p.Y)]; const right = Math.max(...xs); const left = Math.min(...xs); const bottom = Math.max(...ys); const top = Math.min(...ys); - return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top }; + return { right, left, bottom, top, width: right - left, height: bottom - top }; } @computed get svgBounds() { @@ -856,7 +857,7 @@ export class GestureOverlay extends Touchable { @computed get elements() { const width = Number(ActiveInkWidth()); const rect = this._overlayRef.current?.getBoundingClientRect(); - const B = this.svgBounds; + 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; B.right = B.right + width / 2; B.top = B.top - width / 2 - (rect?.y || 0); @@ -867,7 +868,7 @@ export class GestureOverlay extends Touchable { this.props.children, this._palette, [this._strokes.map((l, i) => { - const b = this.getBounds(l); + const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 };//this.getBounds(l, true); return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 89292a445..fb360ee26 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -56,7 +56,7 @@ export class KeyManager { public handle = action(async (e: KeyboardEvent) => { if (e.key?.toLowerCase() === "shift" && e.ctrlKey && e.altKey) KeyManager.Instance.ShiftPressed = true; - if (!Doc.UserDoc().noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.PRINT_CACHE(); + if (!Doc.UserDoc().noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.UPDATE_SERVER_CACHE(true); const keyname = e.key && e.key.toLowerCase(); this.handleGreedy(keyname); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 186406424..7e424d8f0 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -38,7 +38,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume this.props.Document._backgroundColor = "rgba(0,0,0,0.7)"; this.props.Document.mixBlendMode = "hard-light"; this.props.Document.color = "#9b9b9bff"; - this.props.Document._stayInCollection = true; + //this.props.Document._stayInCollection = true; this.props.Document.isInkMask = true; } @@ -190,8 +190,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume return ( <svg className="inkingStroke" - width={Math.max(width, height)} - height={Math.max(width, height)} style={{ pointerEvents: this.props.Document.isInkMask ? "all" : "none", transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 3889e2d28..c256d2ebb 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -13,10 +13,11 @@ AssignAllExtensions(); (async () => { window.location.search.includes("safe") && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); - await Docs.Prototypes.initialize(); if (info.id !== "__guest__") { // a guest will not have an id registered await CurrentUserUtils.loadUserDocument(info.id); + } else { + await Docs.Prototypes.initialize(); } document.getElementById('root')!.addEventListener('wheel', event => { if (event.ctrlKey) { diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index b608eceb1..33bd7e77e 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -153,7 +153,7 @@ cursor: auto; } -.mainView-innerContent { +.mainView-innerContent, .mainView-innerContent-dark { display: contents; flex-direction: row; position: relative; @@ -174,6 +174,46 @@ right: 0; position: absolute; z-index: 2; + background-color: rgb(159, 159, 159); + .editable-title { + background-color: lightgrey; + } + } + +} +.mainView-libraryHandle { + background-color: lightgrey; +} +.mainView-innerContent-dark +{ + .propertiesView { + background-color: #252525; + input { + background-color: dimgrey; + } + .propertiesView-sharingTable + { + background-color: dimgrey; + } + .editable-title { + background-color: dimgrey; + } + .propertiesView-field { + background-color: dimgrey; + } + } + .mainView-propertiesDragger, + .mainView-libraryHandle { + background: #353535; + } +} +.mainView-container-dark { + .contextMenu-cont { + background: dimgrey; + color: white; + input::placeholder { + color:white; + } } } @@ -334,7 +374,6 @@ position: relative; z-index: 41; // lm_maximised has a z-index of 40 and this needs to be above that touch-action: none; - background-color: lightgrey; cursor: grab; display: flex; align-items: center; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 69354020b..452ce61ff 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -58,6 +58,7 @@ import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; import { TraceMobx } from '../../fields/util'; import { SelectionManager } from '../util/SelectionManager'; +import { UndoManager } from '../util/UndoManager'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -132,7 +133,7 @@ export class MainView extends React.Component { } library.add(fa.faEdit, fa.faTrash, fa.faTrashAlt, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faCalendar, - fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faMapMarker, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, + fa.faSquare, far.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faMapMarker, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointLeft, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, fa.faQuestion, fa.faTasks, fa.faPalette, fa.faAngleLeft, fa.faAngleRight, fa.faBell, fa.faCamera, fa.faExpand, fa.faCaretDown, fa.faCaretLeft, fa.faCaretRight, fa.faCaretSquareDown, fa.faCaretSquareRight, fa.faArrowsAltH, fa.faPlus, fa.faMinus, fa.faTerminal, fa.faToggleOn, fa.faFile, fa.faLocationArrow, @@ -150,7 +151,7 @@ export class MainView extends React.Component { fa.faAngleDown, fa.faPlayCircle, fa.faClock, fa.faRocket, fa.faExchangeAlt, faBuffer, fa.faHashtag, fa.faAlignJustify, fa.faCheckSquare, fa.faListUl, fa.faWindowMinimize, fa.faWindowRestore, fa.faTextWidth, fa.faTextHeight, fa.faClosedCaptioning, fa.faInfoCircle, fa.faTag, fa.faSyncAlt, fa.faPhotoVideo, fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical, - fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, + fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll, fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines); this.initAuthenticationRouters(); } @@ -174,6 +175,7 @@ export class MainView extends React.Component { initEventListeners = () => { window.addEventListener("drop", e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page window.addEventListener("dragover", e => e.preventDefault(), false); + document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined)); document.addEventListener("pointerdown", this.globalPointerDown); document.addEventListener("click", (e: MouseEvent) => { if (!e.cancelBubble) { @@ -227,25 +229,29 @@ export class MainView extends React.Component { getContentsHeight = () => this._panelHeight - Number(SEARCH_PANEL_HEIGHT.replace("px", "")); defaultBackgroundColors = (doc: Opt<Doc>, renderDepth: number) => { - if (doc?.type === DocumentType.COL) { - return Doc.IsSystem(doc) ? "lightgrey" : StrCast(renderDepth > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground); - } if (this.darkScheme) { switch (doc?.type) { - case DocumentType.FONTICON: return "white"; + case DocumentType.PRESELEMENT: return "dimgrey"; + case DocumentType.PRES: return "#3e3e3e"; + case DocumentType.FONTICON: return "black"; case DocumentType.RTF || DocumentType.LABEL || DocumentType.BUTTON: return "#2d2d2d"; case DocumentType.LINK: - case DocumentType.COL: if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; + case DocumentType.COL: + return Doc.IsSystem(doc) ? "rgb(62,62,62)" : StrCast(renderDepth > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground); + //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; default: return "black"; } } else { switch (doc?.type) { + case DocumentType.PRESELEMENT: return ""; case DocumentType.FONTICON: return "black"; case DocumentType.RTF: return "#f1efeb"; case DocumentType.BUTTON: case DocumentType.LABEL: return "lightgray"; case DocumentType.LINK: - case DocumentType.COL: if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "lightgray"; + case DocumentType.COL: + return Doc.IsSystem(doc) ? "lightgrey" : StrCast(renderDepth > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground); + //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "lightgray"; default: return "white"; } } @@ -343,7 +349,7 @@ export class MainView extends React.Component { ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} relative={true} - forcedBackgroundColor={() => "lightgrey"} + forcedBackgroundColor={() => this.darkScheme ? "rgb(62,62,62)" : "lightgrey"} /> </div> {this.docButtons} @@ -409,18 +415,18 @@ export class MainView extends React.Component { @computed get mainInnerContent() { return <> {this.menuPanel} - <div className="mainView-innerContent" > + <div className={`mainView-innerContent${this.darkScheme ? "-dark" : ""}`}> {this.flyout} - <div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined }} onPointerDown={this.onFlyoutPointerDown} > - <FontAwesomeIcon icon="chevron-left" color="black" size="sm" /> + < div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined, }} onPointerDown={this.onFlyoutPointerDown} > + <FontAwesomeIcon icon="chevron-left" color={this.darkScheme ? "white" : "black"} size="sm" /> </div> {this.dockingContent} <div className="mainView-propertiesDragger" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}> - <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color="black" size="sm" /> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color={this.darkScheme ? "white" : "black"} size="sm" /> </div> - {this.propertiesWidth() < 10 ? (null) : <PropertiesView width={this.propertiesWidth()} height={this.getContentsHeight()} />} + {this.propertiesWidth() < 10 ? (null) : <PropertiesView backgroundColor={this.defaultBackgroundColors} width={this.propertiesWidth()} height={this.getContentsHeight()} />} </div> </>; } @@ -441,7 +447,7 @@ export class MainView extends React.Component { expandFlyout = action((button: Doc) => { this._flyoutWidth = (this._flyoutWidth || 250); this._sidebarContent.proto = button.target as any; - button._backgroundColor = "lightgrey"; + button._backgroundColor = this.darkScheme ? "dimgrey" : "lightgrey"; button.color = "black"; this._lastButton = button; }); diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index b40c9edfb..7d47abdce 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -14,6 +14,7 @@ import { Scripting } from "../util/Scripting"; import { ScriptingRepl } from './ScriptingRepl'; import { DragManager } from "../util/DragManager"; import { List } from "../../fields/List"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; export type OverlayDisposer = () => void; @@ -146,12 +147,7 @@ export class OverlayView extends React.Component { @computed get overlayDocs() { - const userDocOverlays = Doc.UserDoc().myOverlayDocs; - if (!userDocOverlays) { - return null; - } - return userDocOverlays instanceof Doc && DocListCast(userDocOverlays.data).map(d => { - setTimeout(() => d.inOverlay = true, 0); + return CurrentUserUtils.OverlayDocs?.map(d => { let offsetx = 0, offsety = 0; const dref = React.createRef<HTMLDivElement>(); const onPointerMove = action((e: PointerEvent, down: number[]) => { @@ -161,7 +157,6 @@ export class OverlayView extends React.Component { } if (e.metaKey) { const dragData = new DragManager.DocumentDragData([d]); - d.removeDropProperties = new List<string>(["inOverlay"]); dragData.offset = [-offsetx, -offsety]; dragData.dropAction = "move"; dragData.removeDocument = (doc: Doc | Doc[]) => { diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 9fdc8bc47..1365725cb 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -1,6 +1,4 @@ .propertiesView { - - background-color: rgb(205, 205, 205); height: 100%; font-family: "Noto Sans"; cursor: auto; @@ -9,7 +7,6 @@ overflow-y: auto; .propertiesView-title { - background-color: rgb(159, 159, 159); text-align: center; padding-top: 12px; padding-bottom: 12px; @@ -335,7 +332,6 @@ } } } - .propertiesView-fields { //border-bottom: 1px solid black; //padding: 8.5px; @@ -390,7 +386,7 @@ } } - .field { + .propertiesView-field { display: flex; font-size: 7px; background-color: #e8e8e8; @@ -400,7 +396,7 @@ padding-left: 3px; } - .uneditable-field { + .propertiesView-uneditable-field { display: flex; overflow-y: visible; margin-bottom: 2px; @@ -686,7 +682,17 @@ font-weight: 500; display: inline-flex; + .propertiesView-selectedCount { + width: max-content; + min-width: max-content; + } + .propertiesView-selectedList { + min-width: max-content; + width: 100%; + max-height: 180; + overflow: hidden; + overflow-y: scroll; border-left: solid 1px darkgrey; margin-left: 10px; padding-left: 5px; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 6bdb6e21f..fad2f5284 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -5,7 +5,7 @@ import { intersection } from "lodash"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, WidthSym } from "../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, WidthSym, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkField } from "../../fields/InkField"; import { ComputedField } from "../../fields/ScriptField"; @@ -27,6 +27,7 @@ import { PresBox } from "./nodes/PresBox"; import { PropertiesButtons } from "./PropertiesButtons"; import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; import "./PropertiesView.scss"; +import { CollectionViewType } from "./collections/CollectionView"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -35,6 +36,7 @@ const _global = (window /* browser */ || global /* node */) as any; interface PropertiesViewProps { width: number; height: number; + backgroundColor: (doc: Opt<Doc>, renderDepth: number) => Opt<string>; } @observer @@ -46,7 +48,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } @computed get selectedDocumentView() { if (SelectionManager.SelectedDocuments().length) return SelectionManager.SelectedDocuments()[0]; - if (PresBox.Instance && PresBox.Instance._selectedArray) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); + if (PresBox.Instance?._selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); return undefined; } @computed get isPres(): boolean { @@ -60,7 +62,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openOptions: boolean = true; @observable openSharing: boolean = true; @observable openFields: boolean = true; - @observable openLayout: boolean = true; + @observable openLayout: boolean = false; @observable openContexts: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; @@ -91,9 +93,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { docWidth = () => { if (this.selectedDoc) { const layoutDoc = this.selectedDoc; - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.width - 20)); - return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; + const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._fitWidth); + if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20)); + return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; } else { return 0; } @@ -103,17 +105,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { docHeight = () => { if (this.selectedDoc && this.dataDoc) { const layoutDoc = this.selectedDoc; - return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return this.docWidth() * aspect; - return layoutDoc._fitWidth ? (!this.dataDoc._nativeHeight ? NumCast(this.props.height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, - NumCast(this.props.height)))) : - NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; - })())); - } else { - return 0; + return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, + Doc.NativeAspect(layoutDoc, undefined, true) ? this.docWidth() / Doc.NativeAspect(layoutDoc, undefined, true) : + layoutDoc._fitWidth ? (!Doc.NativeHeight(this.dataDoc) ? NumCast(this.props.height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc)) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height))) : + NumCast(layoutDoc._height) || 50)); } + return 0; } @computed get expandedField() { @@ -147,7 +145,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>); } } - rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> + rows.push(<div className="propertiesView-field" key={"newKeyValue"} style={{ marginTop: "3px" }}> <EditableView key="editableView" contents={"add key:value or #tags"} @@ -175,14 +173,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { docs.forEach(doc => docvals.add(doc[key])); const contents = Array.from(docvals.keys()).length > 1 ? "-multiple" : docs[0][key]; if (key[0] === "#") { - rows.push(<div className="uneditable-field" key={key}> + rows.push(<div className="propertiesView-uneditable-field" key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> </div>); } else if (contents !== undefined) { const value = Field.toString(contents as Field); if (noviceReqFields.includes(key) || key.indexOf("lastModified") !== -1) { - rows.push(<div className="uneditable-field" key={key}> + rows.push(<div className="propertiesView-uneditable-field" key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> </div>); @@ -203,7 +201,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } } } - rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> + rows.push(<div className="propertiesView-field" key={"newKeyValue"} style={{ marginTop: "3px" }}> <EditableView key="editableView" contents={"add key:value or #tags"} @@ -256,7 +254,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return !this.selectedDoc ? (null) : <PropertiesDocContextSelector Document={this.selectedDoc} hideTitle={true} addDocTab={(doc, where) => CollectionDockingView.AddSplit(doc, "right")} />; } - previewBackground = () => "lightgrey"; @computed get layoutPreview() { if (SelectionManager.SelectedDocuments().length > 1) { return "-- multiple selected --"; @@ -273,7 +270,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { renderDepth={1} rootSelected={returnFalse} treeViewDoc={undefined} - backgroundColor={this.previewBackground} + backgroundColor={this.props.backgroundColor} fitToBox={true} FreezeDimensions={true} dontCenter={true} @@ -859,7 +856,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width, - minWidth: this.props.width + minWidth: this.props.width, //overflowY: this.scrolling ? "scroll" : "visible" }} > <div className="propertiesView-title" style={{ width: this.props.width }}> @@ -972,7 +969,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} </div> - {/* <div className="propertiesView-layout"> + <div className="propertiesView-layout"> <div className="propertiesView-layout-title" onPointerDown={action(() => this.openLayout = !this.openLayout)} style={{ backgroundColor: this.openLayout ? "black" : "" }}> @@ -982,12 +979,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> </div> {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} - </div> */} + </div> </div>; } if (this.isPres) { - const selectedItem: boolean = PresBox.Instance?._selectedArray.length > 0; + const selectedItem: boolean = PresBox.Instance?._selectedArray.size > 0; const type = PresBox.Instance.activeItem?.type; + const viewType = PresBox.Instance.activeItem?._viewType; + const pannable: boolean = (type === DocumentType.COL && viewType === CollectionViewType.Freeform) || type === DocumentType.IMG; + const scrollable: boolean = type === DocumentType.PDF || type === DocumentType.WEB || type === DocumentType.RTF || viewType === CollectionViewType.Stacking; return <div className="propertiesView" style={{ width: this.props.width }}> <div className="propertiesView-title" style={{ width: this.props.width }}> Presentation @@ -995,7 +995,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-name"> {this.editableTitle} <div className="propertiesView-presSelected"> - {PresBox.Instance?._selectedArray.length} selected + <div className="propertiesView-selectedCount"> + {PresBox.Instance?._selectedArray.size} selected + </div> <div className="propertiesView-selectedList"> {PresBox.Instance?.listOfSelected} </div> @@ -1014,7 +1016,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {PresBox.Instance.transitionDropdown} </div> : null} </div>} - {!selectedItem || type === DocumentType.VID || type === DocumentType.AUDIO ? (null) : <div className="propertiesView-presTrails"> + {/* {!selectedItem || type === DocumentType.VID || type === DocumentType.AUDIO ? (null) : <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" onPointerDown={action(() => this.openPresProgressivize = !this.openPresProgressivize)} style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}> @@ -1026,20 +1028,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.openPresProgressivize ? <div className="propertiesView-presTrails-content"> {PresBox.Instance.progressivizeDropdown} </div> : null} - </div>} - {!selectedItem || (type !== DocumentType.COL && type !== DocumentType.VID && type !== DocumentType.AUDIO) ? (null) : <div className="propertiesView-presTrails"> + </div>} */} + {/* {!selectedItem || (!scrollable && !pannable) ? (null) : <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" onPointerDown={action(() => { this.openSlideOptions = !this.openSlideOptions; })} style={{ backgroundColor: this.openSlideOptions ? "black" : "" }}> - <FontAwesomeIcon icon={"cog"} /> {PresBox.Instance.stringType} options - <div className="propertiesView-presTrails-title-icon"> + <FontAwesomeIcon icon={"cog"} /> {scrollable ? "Scroll options" : "Pan options"} + <div className="propertiesView-presTrails-title-icon"> <FontAwesomeIcon icon={this.openSlideOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> {this.openSlideOptions ? <div className="propertiesView-presTrails-content"> {PresBox.Instance.optionsDropdown} </div> : null} - </div>} + </div>} */} {/* <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })} diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 8b1594b21..f4736eb29 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -4,7 +4,7 @@ .lm_title { margin-top: 3px; border-radius: 5px; - border: solid 1px dimgray; + border: solid 0px dimgray; border-width: 2px 2px 0px; height: 20px; transform: translate(0px, -3px); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 2eaa284cc..abe8477e4 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -22,6 +22,8 @@ import { CollectionViewType } from './CollectionView'; import { TabDocView } from './TabDocView'; import React = require("react"); import { stat } from 'fs'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { listSpec } from '../../../fields/Schema'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -108,6 +110,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { } @undoBatch + @action public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean { const instance = CollectionDockingView.Instance; if (!instance) return false; @@ -140,8 +143,21 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // @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); + if (tab) { + tab.header.parent.setActiveContentItem(tab.contentItem); + return true; + } const instance = CollectionDockingView.Instance; if (!instance) return false; const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); @@ -308,9 +324,15 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { @action onPointerDown = (e: React.PointerEvent): void => { - window.addEventListener("mouseup", this.onPointerUp); - if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) { - this._flush = UndoManager.StartBatch("golden layout edit"); + let hitFlyout = false; + for (let par = e.target as any; !hitFlyout && par; par = par.parentElement) { + hitFlyout = (par.className === "dockingViewButtonSelector"); + } + if (!hitFlyout) { + window.addEventListener("mouseup", this.onPointerUp); + if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) { + this._flush = UndoManager.StartBatch("golden layout edit"); + } } if (!e.nativeEvent.cancelBubble && !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.GetSelectedTool() !== InkTool.Highlighter && Doc.GetSelectedTool() !== InkTool.Pen) { diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 0eac5136a..b6ab3f0e0 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -129,7 +129,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { {this.childLayoutPairs.map((pair, ind) => { const nested = pair.layout._viewType === CollectionViewType.Linear; const dref = React.createRef<HTMLDivElement>(); - const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension()); const scalable = pair.layout.onClick || pair.layout.onDragStart; return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 7014966c7..b35644c6b 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -138,7 +138,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr } @action - addDocument = (value: string, shiftDown?: boolean) => { + addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { + if (!value && !forceEmptyNote) return false; this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document._pivotField); const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _width: 200, title: value }); @@ -239,6 +240,10 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr </div> </div>); } + @action + textCallback = (char: string) => { + return this.addDocument("", false); + } @computed get contentLayout() { const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); @@ -247,6 +252,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const newEditableViewProps = { GetValue: () => "", SetValue: this.addDocument, + textCallback: this.textCallback, contents: "+ NEW", HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index b2b1b7d25..b2b23115f 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -344,7 +344,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @computed get viewModes() { const excludedViewTypes = Doc.UserDoc().noviceMode ? [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.Map, CollectionViewType.Linear, CollectionViewType.Time] : [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.Linear]; - return <div className="collectionViewBaseChrome-viewModes" > + const isPres: boolean = (this.document && this.document.type === DocumentType.PRES); + return isPres ? (null) : (<div className="collectionViewBaseChrome-viewModes" > <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> <div className="commandEntry-outerDiv" ref={this._viewRef} onPointerDown={this.dragViewDown}> <button className={"antimodeMenu-button"}> @@ -367,7 +368,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </select> </div> </Tooltip> - </div>; + </div>); } @computed get selectedDocumentView() { @@ -399,12 +400,15 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp pinWithView = (targetDoc: Opt<Doc>) => { if (targetDoc) { TabDocView.PinDoc(targetDoc, false); - const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB) { + const presArray: Doc[] = PresBox.Instance?.sortArray(); + const size: number = PresBox.Instance?._selectedArray.size; + const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; + const activeDoc = presSelected ? PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.indexOf(presSelected) + 1] : PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.length - 1]; + if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) { const scroll = targetDoc._scrollTop; activeDoc.presPinView = true; activeDoc.presPinViewScroll = scroll; - } else { + } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) { const x = targetDoc._panX; const y = targetDoc._panY; const scale = targetDoc._viewScale; @@ -412,6 +416,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp activeDoc.presPinViewX = x; activeDoc.presPinViewY = y; activeDoc.presPinViewScale = scale; + } else if (targetDoc.type === DocumentType.VID) { + activeDoc.presPinTimecode = targetDoc._currentTimecode; + activeDoc.presPinView = true; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const width = targetDoc._clipWidth; + activeDoc.presPinClipWidth = width; + activeDoc.presPinView = true; } } } @@ -421,7 +432,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: "auto", width: 19 }} />; const targetDoc = this.selectedDoc; {/* return (!targetDoc || (targetDoc._viewType !== CollectionViewType.Freeform && targetDoc.type !== DocumentType.IMG)) ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation trail with current view"}</div></>} placement="top"> */ } - return (targetDoc && (targetDoc._viewType === CollectionViewType.Freeform || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF)) ? <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation trail with current view"}</div></>} placement="top"> + return (targetDoc && targetDoc.type !== DocumentType.PRES && (targetDoc._viewType === CollectionViewType.Freeform || targetDoc._viewType === CollectionViewType.Stacking || targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.COMPARISON)) ? <Tooltip title={<><div className="dash-tooltip">{"Pin with current view"}</div></>} placement="top"> <button className="antimodeMenu-button" style={{ borderRight: "1px solid gray", borderLeft: "1px solid gray", justifyContent: 'center' }} onClick={() => this.pinWithView(targetDoc)}> {presPinWithViewIcon} @@ -468,7 +479,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @computed get aliasButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Tap or Drag to create an alias"}</div>} placement="top"> + return !targetDoc || targetDoc.type === DocumentType.PRES ? (null) : <Tooltip title={<div className="dash-tooltip">{"Tap or Drag to create an alias"}</div>} placement="top"> <button className="antimodeMenu-button" onPointerDown={this.onAliasButtonDown} onClick={this.onAlias} style={{ cursor: "drag" }}> <FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" /> </button> @@ -478,13 +489,19 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @computed get lightboxButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Lightbox of Images"}</div>} placement="top"> - <button className="antimodeMenu-button" onPointerDown={action(() => targetDoc._isLightboxOpen = true)} onClick={this.onAlias}> + <button className="antimodeMenu-button" onPointerDown={action(() => targetDoc._isLightboxOpen = true)}> <FontAwesomeIcon className="documentdecorations-icon" icon="desktop" size="lg" /> </button> </Tooltip>; } - - + @computed get gridbackgroundButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Toggle background grid"}</div>} placement="top"> + <button className="antimodeMenu-button" onPointerDown={action(() => targetDoc["_backgroundGrid-show"] = !targetDoc["_backgroundGrid-show"])}> + <FontAwesomeIcon className="documentdecorations-icon" icon={targetDoc["_backgroundGrid-show"] ? "border-all" : ["far", "square"]} size="lg" /> + </button> + </Tooltip>; + } render() { return ( @@ -502,6 +519,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </button> </Tooltip>} {this.notACollection ? (null) : this.lightboxButton} + {this.notACollection ? (null) : this.gridbackgroundButton} {this.aliasButton} {/* {this.pinButton} */} {this.pinWithViewButton} @@ -529,7 +547,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } get document() { return this.props.docView.props.Document; } @computed get dataField() { - return this.document[this.props.docView.LayoutFieldKey]; + return this.document[this.props.docView.LayoutFieldKey + (this.props.isOverlay ? "-annotations" : "")]; } @computed get childDocs() { return DocListCast(this.dataField); @@ -540,7 +558,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get isText() { - return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any)?.focused ? true : false; + return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; } @undoBatch @@ -757,6 +775,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } else { history.push(this._url); } + this.props.docView.props.Document._scrollTop = 0; future && (future.length = 0); } this._url = url; @@ -1312,5 +1331,5 @@ Scripting.addGlobal(function gotoFrame(doc: any, newFrame: any) { CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); - doc._currentFrame = Math.max(0, newFrame); + doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); }); diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 5b4730848..2636b98e5 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -6,7 +6,7 @@ import { emptyFunction, setupMoveUpEvents } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; -import { UndoManager } from "../../util/UndoManager"; +import { UndoManager, undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import "./CollectionPileView.scss"; import { CollectionSubView } from "./CollectionSubView"; @@ -34,14 +34,14 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { const draggingSelf = this.props.isSelected(); return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" || (SnappingManager.GetIsDragging() && !draggingSelf) ? undefined : "none", zIndex: this.layoutEngine() === "starburst" && !SnappingManager.GetIsDragging() ? -10 : "auto" }} > <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} - addDocument={(doc: Doc | Doc[]) => { + addDocument={undoBatch((doc: Doc | Doc[]) => { (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d)); return this.props.addDocument(doc); - }} - moveDocument={(doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - (doc instanceof Doc ? [doc] : doc).map((d) => Doc.deiconifyView(d)); + })} + moveDocument={undoBatch((doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { + (doc instanceof Doc ? [doc] : doc).map(undoBatch((d) => Doc.deiconifyView(d))); return this.props.moveDocument(doc, targetCollection, addDoc); - }} /> + })} /> </div>; } @@ -107,6 +107,8 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } // onClick for toggling the pileup view + @undoBatch + @action onClick = (e: React.MouseEvent) => { if (e.button === 0) { SelectionManager.DeselectAll(); diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 2896a8243..0b3dfe1e4 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -398,10 +398,10 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell { const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - const heightToWidth = NumCast(this._rowDoc._nativeHeight) / NumCast(this._rowDoc._nativeWidth); + const aspect = Doc.NativeAspect(this._rowDoc); let width = Math.min(75, this.props.rowProps.width); - const height = Math.min(75, width * heightToWidth); - width = height / heightToWidth; + const height = Math.min(75, width / aspect); + width = height * aspect; const reference = React.createRef<HTMLDivElement>(); return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index fc4ca3100..97eacaeab 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -28,6 +28,7 @@ import { CollectionViewType } from "./CollectionView"; import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocUtils } from "../../documents/Documents"; +import { DocAfterFocusFunc } from "../nodes/DocumentView"; const _global = (window /* browser */ || global /* node */) as any; type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>; @@ -59,7 +60,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } @computed get showAddAGroup() { return (this.pivotField && (this.chromeStatus !== 'view-mode' && this.chromeStatus !== 'disabled')); } @computed get columnWidth() { - return Math.min(this.props.PanelWidth() / this.props.ContentScaling() - 2 * this.xMargin, + return Math.min(this.props.PanelWidth() / this.props.ContentScaling() /* / NumCast(this.layoutDoc._viewScale, 1)*/ - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } @@ -170,9 +171,9 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, } - focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { + focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, didFocus?: boolean) => { Doc.BrushDoc(doc); - this.props.focus(doc); + this.props.focus(this.props.Document, true); // bcz: want our containing collection to zoom Doc.linkFollowHighlight(doc); const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName("documentView-node")).find((node: any) => node.id === doc[Id]); @@ -233,27 +234,27 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, getDocWidth(d?: Doc) { if (!d) return 0; const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); - const nw = NumCast(layoutDoc._nativeWidth); + const nw = Doc.NativeWidth(layoutDoc); return Math.min(nw && !this.layoutDoc._columnsFill ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); } getDocHeight(d?: Doc) { if (!d) return 0; - const dataDoc = (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc; - const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); - const layoutField = Doc.LayoutFieldKey(layoutDoc); - const nw = NumCast(layoutDoc._nativeWidth) || NumCast(dataDoc?.[`${layoutField}-nativeWidth`]); - const nh = NumCast(layoutDoc._nativeHeight) || NumCast(dataDoc?.[`${layoutField}-nativeHeight`]); + const childDataDoc = (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc; + const childLayoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc); let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); + if (!this.layoutDoc._columnsFill) wid = Math.min(wid, childLayoutDoc[WidthSym]()); const hllimit = NumCast(this.layoutDoc.childLimitHeight, -1); - if (!layoutDoc._fitWidth && nw && nh) { + if (!childLayoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; if (!(this.layoutDoc._columnsFill)) wid = Math.min(this.getDocWidth(d), wid); return Math.min(hllimit === 0 ? this.props.PanelWidth() : hllimit === -1 ? 10000 : hllimit, wid * aspect); } - return layoutDoc._fitWidth ? + return childLayoutDoc._fitWidth ? (!nh ? this.props.PanelHeight() - 2 * this.yMargin : Math.min(wid * nh / (nw || 1), this.layoutDoc._autoHeight ? 100000 : this.props.PanelHeight() - 2 * this.yMargin)) : - Math.min(hllimit === 0 ? this.props.PanelWidth() : hllimit === -1 ? 10000 : hllimit, Math.max(20, layoutDoc[HeightSym]())); + Math.min(hllimit === 0 ? this.props.PanelWidth() : hllimit === -1 ? 10000 : hllimit, Math.max(20, childLayoutDoc[HeightSym]())); } columnDividerDown = (e: React.PointerEvent) => { @@ -277,30 +278,30 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const where = [de.x, de.y]; - let targInd = -1; - let plusOne = 0; + let dropInd = -1; + let dropAfter = 0; if (de.complete.docDragData) { this._docXfs.map((cd, i) => { const pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); - if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { - targInd = i; + if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && (i === this._docXfs.length - 1 || where[1] < pos1[1])) { + dropInd = i; const axis = this.Document._viewType === CollectionViewType.Masonry ? 0 : 1; - plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0; + dropAfter = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0; } }); + const oldDocs = this.childDocs.length; if (super.onInternalDrop(e, de)) { - const newDocs = de.complete.docDragData.droppedDocuments; + const newDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= oldDocs); + + //de.complete.docDragData.droppedDocuments; const docs = this.childDocList; DragManager.docsBeingDragged = []; - if (docs) { - newDocs.map((doc, i) => { - targInd = targInd === -1 ? docs.length : targInd; - const srcInd = docs.indexOf(doc); - if (targInd !== -1) targInd = i === 0 ? docs.indexOf(this.filteredChildren[targInd]) : docs.indexOf(newDocs[0]) + 1; - docs.splice(srcInd, 1); - docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc); - }); + if (docs && newDocs.length) { + const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter; + const offset = newDocs.reduce((off, ndoc) => this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off, 0); + newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1)); + docs.splice(insertInd - offset, 0, ...newDocs); } } } @@ -351,8 +352,12 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { - const height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); - Doc.Layout(doc)._height = Math.max(height, NumCast(doc[this.props.fieldKey + "-height"])); + const height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => NumCast(Doc.Layout(doc)._viewScale, 1) * Number(getComputedStyle(r).height.replace("px", ""))))); + if (this.props.annotationsKey) { + doc[this.props.annotationsKey + "-height"] = height; + } else { + Doc.Layout(doc)._height = height * NumCast(Doc.Layout(doc)._viewScale, 1); + } } })); this.observer.observe(ref); @@ -405,7 +410,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, this.observer = new _global.ResizeObserver(action((entries: any) => { if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); - Doc.Layout(doc)._height = Math.max(height, NumCast(doc[this.props.fieldKey + "-height"])); + Doc.Layout(doc)._height = Math.max(height * NumCast(doc[this.props.fieldKey + "-height"]), NumCast(doc[this.props.fieldKey + "-height"])); } })); this.observer.observe(ref); @@ -465,8 +470,8 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, } - @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), NumCast(this.layoutDoc._nativeWidth)); } - @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), NumCast(this.layoutDoc._nativeHeight)); } + @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc)); } + @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc)); } @computed get scaling() { return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index f3e563422..b27f64ff0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -344,12 +344,16 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc).text = text; this.props.addDocument(htmlDoc); if (srcWeb) { - const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")?.[0]?.contentDocument?.getSelection()?.focusNode as any); + const iframe = SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")?.[0]; + const focusNode = (iframe?.contentDocument?.getSelection()?.focusNode as any); if (focusNode) { - const rect = "getBoundingClientRect" in focusNode ? focusNode.getBoundingClientRect() : focusNode?.parentElement.getBoundingClientRect(); - const x = (rect?.x || 0); - const y = NumCast(srcWeb._scrollTop) + (rect?.y || 0); - const anchor = Docs.Create.FreeformDocument([], { _backgroundColor: "transparent", _width: 75, _height: 40, x, y, annotationOn: srcWeb }); + const rects = iframe?.contentWindow?.getSelection()?.getRangeAt(0).getClientRects(); + "getBoundingClientRect" in focusNode ? focusNode.getBoundingClientRect() : focusNode?.parentElement.getBoundingClientRect(); + const x = (rects && Array.from(rects).reduce((x: any, r: DOMRect) => x === undefined || r.x < x ? r.x : x, undefined as any)) || 0; + const y = NumCast(srcWeb._scrollTop) + ((rects && Array.from(rects).reduce((y: any, r: DOMRect) => y === undefined || r.y < y ? r.y : y, undefined as any)) || 0); + const r = (rects && Array.from(rects).reduce((x: any, r: DOMRect) => x === undefined || r.x + r.width > x ? r.x + r.width : x, undefined as any)) || 0; + const b = NumCast(srcWeb._scrollTop) + ((rects && Array.from(rects).reduce((y: any, r: DOMRect) => y === undefined || r.y + r.height > y ? r.y + r.height : y, undefined as any)) || 0); + const anchor = Docs.Create.FreeformDocument([], { _backgroundColor: "transparent", _width: r - x, _height: b - y, x, y, annotationOn: srcWeb }); anchor.context = srcWeb; const key = Doc.LayoutFieldKey(srcWeb); Doc.AddDocToList(srcWeb, key + "-annotations", anchor); @@ -406,7 +410,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: this.addDocument(alias); } else { console.log("Adding ..."); - const newDoc = Docs.Create.WebDocument(uriList, { + const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) ...options, _fitWidth: true, title: uriList.split("#annotations:")[0], @@ -416,7 +420,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: useCors: true }); console.log(" ... " + newDoc.title); - newDoc.data = new WebField(uriList.split("#annotations:")[0]); // clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) console.log(" ... " + this.addDocument(newDoc) + " " + newDoc.title); } return; @@ -479,6 +482,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } else { if (text && !text.includes("https://")) { UndoManager.RunInBatch(() => this.addDocument(Docs.Create.TextDocument(text, { ...options, title: text.substring(0, 20), _width: 400, _height: 315 })), "drop"); + } else { + alert("Document upload failed - possibly an unsupported file type."); } } disposer(); diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index edf556c9f..9acbc4f85 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -43,7 +43,6 @@ input.lm_title { right: 0; width: 45px; height: 45px; - background: white; transform: translate(20px, 20px) rotate(45deg); border-radius: 30px; padding: 2px; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 0c7f39dc7..530595cd0 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -6,7 +6,7 @@ import { clamp } from 'lodash'; import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; -import { DataSym, Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { DataSym, Doc, DocListCast, Opt, DocListCastAsync } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; import { listSpec } from '../../../fields/Schema'; @@ -21,7 +21,7 @@ import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { DocumentView } from "../nodes/DocumentView"; +import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView"; import { PresBox, PresMovement } from '../nodes/PresBox'; import { CollectionDockingView } from './CollectionDockingView'; import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; @@ -39,15 +39,14 @@ interface TabDocViewProps { export class TabDocView extends React.Component<TabDocViewProps> { _mainCont: HTMLDivElement | null = null; _tabReaction: IReactionDisposer | undefined; + @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _isActive: boolean = false; @observable private _document: Doc | undefined; @observable private _view: DocumentView | undefined; - @computed get contentScaling() { return this.ContentScaling(); } - - get stack(): any { return (this.props as any).glContainer.parent.parent; } + get stack() { return (this.props as any).glContainer.parent.parent; } get tab() { return (this.props as any).glContainer.tab; } get view() { return this._view; } @@ -63,10 +62,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { const titleEle = tab.titleElement[0]; titleEle.size = StrCast(doc.title).length + 3; titleEle.value = doc.title; - titleEle.onchange = (e: any) => { + titleEle.onchange = undoBatch(action((e: any) => { titleEle.size = e.currentTarget.value.length + 3; Doc.GetProto(doc).title = e.currentTarget.value; - }; + })); // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: MouseEvent) => { if (SnappingManager.GetIsDragging() && tab.contentItem !== tab.header.parent.getActiveContentItem()) { @@ -122,29 +121,41 @@ export class TabDocView extends React.Component<TabDocViewProps> { /** * Adds a document to the presentation view **/ - @undoBatch @action - public static PinDoc(doc: Doc, unpin = false, audioRange?: boolean) { + public static async PinDoc(doc: Doc, unpin = false, audioRange?: boolean) { if (unpin) console.log('TODO: Remove UNPIN from this location'); //add this new doc to props.Document const curPres = CurrentUserUtils.ActivePresentation; 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; + pinDoc.title = doc.title + " - Slide"; pinDoc.presMovement = PresMovement.Zoom; pinDoc.context = curPres; - Doc.AddDocToList(curPres, "data", pinDoc); + const presArray: Doc[] = PresBox.Instance?.sortArray(); + const size: number = PresBox.Instance?._selectedArray.size; + const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; + Doc.AddDocToList(curPres, "data", pinDoc, presSelected); if (pinDoc.type === "audio" && !audioRange) { pinDoc.presStartTime = 0; pinDoc.presEndTime = doc.duration; } if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - const curPresDocView = DocumentManager.Instance.getDocumentView(curPres); - if (!curPresDocView) { + const dview = CollectionDockingView.Instance.props.Document; + const fieldKey = CollectionDockingView.Instance.props.fieldKey; + const sublists = DocListCast(dview[fieldKey]); + const tabs = Cast(sublists[0], Doc, null); + const tabdocs = await DocListCastAsync(tabs.data); + 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"); } - DocumentManager.Instance.jumpToDocument(doc, false, undefined, Cast(doc.context, Doc, null)); + PresBox.Instance?._selectedArray.clear(); + pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array + DocumentManager.Instance.jumpToDocument(doc, false, undefined); + batch.end(); } } @@ -190,23 +201,12 @@ export class TabDocView extends React.Component<TabDocViewProps> { } } - nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; - panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : - (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) - panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; - nativeWidth = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeHeight) || this._panelHeight : 0; - ContentScaling = () => { - const nativeH = NumCast(this.layoutDoc?._nativeHeight); - const nativeW = NumCast(this.layoutDoc?._nativeWidth); - let scaling = 1; - if (nativeW && (this.layoutDoc?._fitWidth || this._panelHeight / nativeH > this._panelWidth / nativeW)) { - scaling = this._panelWidth / nativeW; // width-limited or fitWidth - } else if (nativeW && nativeH) { - scaling = this._panelHeight / nativeH; // height-limited - } - return scaling; - } + NativeAspect = () => this.nativeAspect; + PanelWidth = () => this.panelWidth; + PanelHeight = () => this.panelHeight; + nativeWidth = () => this._nativeWidth; + nativeHeight = () => this._nativeHeight; + ContentScaling = () => this.contentScaling; ScreenToLocalTransform = () => { if (this._mainCont?.children) { @@ -216,6 +216,29 @@ export class TabDocView extends React.Component<TabDocViewProps> { } return Transform.Identity(); } + @computed get nativeAspect() { + return this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; + } + @computed get panelHeight() { + return this.NativeAspect() && this.NativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.NativeAspect() : this._panelHeight; + } + @computed get panelWidth() { + return this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), Doc.NativeWidth(this.layoutDoc)), this._panelWidth) : + (this.NativeAspect() && this.NativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.NativeAspect() : this._panelWidth); + } + @computed get _nativeWidth() { return !this.layoutDoc?._fitWidth ? Doc.NativeWidth(this.layoutDoc) || this._panelWidth : 0; } + @computed get _nativeHeight() { return !this.layoutDoc?._fitWidth ? Doc.NativeHeight(this.layoutDoc) || this._panelHeight : 0; } + @computed get contentScaling() { + const nativeW = Doc.NativeWidth(this.layoutDoc); + const nativeH = Doc.NativeHeight(this.layoutDoc); + let scaling = 1; + if (nativeW && (this.layoutDoc?._fitWidth || this._panelHeight / nativeH > this._panelWidth / nativeW)) { + scaling = this._panelWidth / nativeW; // width-limited or fitWidth + } else if (nativeW && nativeH) { + scaling = this._panelHeight / nativeH; // height-limited + } + return scaling; + } @computed get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.ContentScaling()) / 2 : 0; } @computed get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.ContentScaling()) / this._panelWidth * 100}% ` : undefined; } @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } @@ -265,8 +288,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); } renderMiniMap() { - const miniWidth = this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; - const miniHeight = this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; + const miniWidth = this.PanelWidth() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; + const miniHeight = this.PanelHeight() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; const miniLeft = 50 + (NumCast(this._document?._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; const miniTop = 50 + (NumCast(this._document?._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; const miniSize = this.returnMiniSize(); @@ -313,17 +336,18 @@ export class TabDocView extends React.Component<TabDocViewProps> { </div> <Tooltip title={<div className="dash-tooltip">{"toggle minimap"}</div>}> - <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} + style={{ background: CollectionDockingView.Instance.props.backgroundColor?.(this._document, 0) }} > <FontAwesomeIcon icon={"globe-asia"} size="lg" /> </div> </Tooltip> </>; } - focusFunc = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => void) => { + focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, notFocused?: boolean) => { if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) } - afterFocus?.(); + afterFocus?.(false); } setView = action((view: DocumentView) => this._view = view); active = () => this._isActive; @@ -340,8 +364,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { addDocument={undefined} removeDocument={undefined} ContentScaling={this.ContentScaling} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} NativeHeight={this.nativeHeight() ? this.nativeHeight : undefined} NativeWidth={this.nativeWidth() ? this.nativeWidth : undefined} ScreenToLocalTransform={this.ScreenToLocalTransform} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 1dfa5615a..925eb4be6 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -279,21 +279,21 @@ export class TreeView extends React.Component<TreeViewProps> { getTransform = () => this.refTransform(this._tref.current!); docWidth = () => { const layoutDoc = this.layoutDoc; - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + const aspect = Doc.NativeAspect(layoutDoc); + if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - 20)); + return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } docHeight = () => { const layoutDoc = this.layoutDoc; const bounds = this.boundsOfCollectionDocument; return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return this.docWidth() * aspect; + const aspect = Doc.NativeAspect(layoutDoc); + if (aspect) return this.docWidth() / (aspect || 1); if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc._fitWidth ? (!this.doc._nativeHeight ? NumCast(this.props.containingCollection._height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, - NumCast(this.props.containingCollection._height)))) : - NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; + return layoutDoc._fitWidth ? (!Doc.NativeHeight(this.doc) ? NumCast(this.props.containingCollection._height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc)) / + (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containingCollection._height)) + )) : (layoutDoc[HeightSym]() || 50); })())); } @@ -728,7 +728,7 @@ export class TreeView extends React.Component<TreeViewProps> { const addDocument = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false); const childLayout = Doc.Layout(pair.layout); const rowHeight = () => { - const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0); + const aspect = Doc.NativeAspect(childLayout); return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); }; return <TreeView key={child[Id]} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index bc2cb2d20..edf473a90 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -241,10 +241,10 @@ export function computePivotLayout( val.docs.forEach((doc, i) => { const layoutDoc = Doc.Layout(doc); let wid = pivotAxisWidth; - let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth; + let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1); if (hgt > pivotAxisWidth) { hgt = pivotAxisWidth; - wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth; + wid = (Doc.NativeAspect(layoutDoc) || 1) * pivotAxisWidth; } docMap.set(doc[Id] + (val.replicas || ""), { x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? (numCols - val.docs.length) * pivotAxisWidth / 2 : 0), @@ -368,10 +368,10 @@ export function computeTimelineLayout( const stack = findStack(x, stacking); const layoutDoc = Doc.Layout(doc); let wid = pivotAxisWidth; - let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth; + let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1); if (hgt > pivotAxisWidth) { hgt = pivotAxisWidth; - wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth; + wid = (Doc.NativeAspect(layoutDoc) || 1) * pivotAxisWidth; } docMap.set(doc[Id], { x: x, y: -Math.sqrt(stack) * pivotAxisWidth / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 75cbc20ca..a50b41198 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -12,6 +12,13 @@ border-radius: inherit; } +.collectionfreeformview-grid { + transform-origin: top left; + position: absolute; + top: 0; + left: 0; +} + .collectionfreeformview-viewdef { >.collectionFreeFormDocumentView-container { pointer-events: none; @@ -57,7 +64,7 @@ min-height: 15px; text-align: center; background-color: #69a6db; - border-radius: 5px; + border-radius: 10%; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); font-family: Roboto; font-weight: 500; @@ -65,6 +72,13 @@ } } +.pathOrder-presPinView { + position: absolute; + z-index: 190000; + border-style: dashed; + border-color: #69a5db; +} + .progressivizeButton { position: absolute; display: grid; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b32a3bd52..1033050b9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -33,7 +33,7 @@ import { ContextMenu } from "../../ContextMenu"; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentViewProps, DocAfterFocusFunc } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import { PresBox } from "../../nodes/PresBox"; @@ -46,6 +46,7 @@ import "./CollectionFreeFormView.scss"; import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; export const panZoomSchema = createSchema({ _panX: "number", @@ -105,11 +106,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable canPanY: boolean = true; @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } - @computed get fitToContent() { return this.props.fitToBox || (this.Document._fitToBox && !this.isAnnotationOverlay); } + @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent ? this.props.ContentScaling() : 1; } @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 : returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); } - @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); } + @computed get nativeWidth() { return this.fitToContent ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); } + @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document)); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } @@ -120,7 +121,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (this.fitToContent) { const zs = !this.childDocs.length ? 1 : Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)); - return mult * (this.props.isAnnotationOverlay ? Math.min(zs, 1) : zs); + return mult * zs; } return mult * NumCast(this.Document[this.scaleFieldKey], 1); } @@ -168,14 +169,23 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (newBox.activeFrame !== undefined) { const x = newBox.x; const y = newBox.y; + const w = newBox._width; + const h = newBox._height; delete newBox["x-indexed"]; delete newBox["y-indexed"]; + delete newBox["w-indexed"]; + delete newBox["h-indexed"]; delete newBox["opacity-indexed"]; + delete newBox._width; + delete newBox._height; delete newBox.x; delete newBox.y; + delete newBox.opacity; delete newBox.activeFrame; newBox.x = x; newBox.y = y; + newBox._width = w; + newBox._height = h; } } if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { @@ -202,13 +212,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { if (!super.onInternalDrop(e, de)) return false; + const refDoc = docDragData.droppedDocuments[0]; const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y); - const z = NumCast(docDragData.droppedDocuments[0].z); + const z = NumCast(refDoc.z); const x = (z ? xpo : xp) - docDragData.offset[0]; const y = (z ? ypo : yp) - docDragData.offset[1]; const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); zsorted.forEach((doc, index) => doc.zIndex = doc.isInkMask ? 5000 : index + 1); - const dropPos = [NumCast(docDragData.droppedDocuments[0].x), NumCast(docDragData.droppedDocuments[0].y)]; + const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); + const dropPos = this.Document._currentFrame !== undefined ? [dvals.x, dvals.y] : [NumCast(refDoc.x), NumCast(refDoc.y)]; for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); @@ -219,7 +231,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; } - const nd = [NumCast(layoutDoc._nativeWidth), NumCast(layoutDoc._nativeHeight)]; + const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300); !d._isBackground && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront @@ -801,7 +813,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.layoutDoc._lockedTransform || this.props.Document.inOverlay || this.props.Document.treeViewOutlineMode) return; + if (this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.props.Document) || this.props.Document.treeViewOutlineMode) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } @@ -837,7 +849,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P else if (ranges.yrange.max <= (panY - panelDim[1] / 2)) panY = ranges.yrange.min - panelDim[1] / 2; } } - if (!this.layoutDoc._lockedTransform || this.Document.inOverlay) { + if (!this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.Document)) { this.Document._viewTransition = panType; const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); @@ -875,7 +887,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1]; } - focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean, dontCenter?: boolean) => { + focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, didFocus?: boolean) => { const state = HistoryUtil.getState(); // TODO This technically isn't correct if type !== "doc", as @@ -914,8 +926,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P afterFocus && setTimeout(afterFocus, delay); } else { !dontCenter && delay && this.props.focus(this.props.Document); - // @ts-ignore - afterFocus(true); // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll + afterFocus?.(!dontCenter && delay ? true : false); } } @@ -925,8 +936,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; willZoom && this.setScaleToZoom(layoutdoc, scale); - const newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (NumCast(this.props.Document._nativeWidth)) / 2 / this.zoomScaling() : 0); - const newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (NumCast(this.props.Document._nativeHeight)) / 2 / this.zoomScaling() : 0); + const newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeWidth(this.props.Document)) / 2 / this.zoomScaling() : 0); + const newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeHeight(this.props.Document)) / 2 / this.zoomScaling() : 0); const newState = HistoryUtil.getState(); newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); @@ -936,26 +947,30 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow } Doc.BrushDoc(this.props.Document); - this.props.focus(this.props.Document); - Doc.linkFollowHighlight(doc); - const notFocused = newPanX === savedState.px && newPanY === savedState.py; - afterFocus && setTimeout(() => { - // @ts-ignore - if (afterFocus?.(notFocused)) { // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll - this.Document._panX = savedState.px; - this.Document._panY = savedState.py; - this.Document[this.scaleFieldKey] = savedState.s; - this.Document._viewTransition = savedState.pt; - } - }, notFocused ? 0 : 500); + const newDidFocus = didFocus || (newPanX !== savedState.px || newPanY !== savedState.py); + + const newAfterFocus = (didFocus: boolean) => { + afterFocus && setTimeout(() => { + // @ts-ignore + if (afterFocus?.(didFocus || (newPanX !== savedState.px || newPanY !== savedState.py))) { + this.Document._panX = savedState.px; + this.Document._panY = savedState.py; + this.Document[this.scaleFieldKey] = savedState.s; + this.Document._viewTransition = savedState.pt; + } + }, newPanX !== savedState.px || newPanY !== savedState.py ? 500 : 0); + return false; + }; + this.props.focus(this.props.Document, undefined, undefined, newAfterFocus, undefined, newDidFocus); + Doc.linkFollowHighlight(doc); } } setScaleToZoom = (doc: Doc, scale: number = 0.75) => { - const pw = this.isAnnotationOverlay ? NumCast(this.props.Document._nativeWidth) : this.props.PanelWidth(); - const ph = this.isAnnotationOverlay ? NumCast(this.props.Document._nativeHeight) : this.props.PanelHeight(); + const pw = this.isAnnotationOverlay ? Doc.NativeWidth(this.props.Document) : this.props.PanelWidth(); + const ph = this.isAnnotationOverlay ? Doc.NativeHeight(this.props.Document) : this.props.PanelHeight(); pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height))); } @@ -979,7 +994,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate, LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString, FreezeDimensions: this.props.freezeChildDimensions, - layoutKey: StrCast(this.props.Document.childLayoutKey), setupDragLines: this.setupDragLines, dontRegisterView: this.props.dontRegisterView, rootSelected: childData ? this.rootSelected : returnFalse, @@ -1207,7 +1221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if ((e as any).handlePan || this.props.isAnnotationOverlay) return; (e as any).handlePan = true; - if (!this.props.Document._noAutoscroll && this._marqueeRef?.current) { + if (!this.props.Document._noAutoscroll && !this.props.renderDepth && this._marqueeRef?.current) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef.current?.getBoundingClientRect(); @@ -1297,7 +1311,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); this.props.renderDepth && optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); if (!Doc.UserDoc().noviceMode) { - optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); + optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" }); } @@ -1423,6 +1437,44 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } return false; }); + + chooseGridSpace = (gridSpace: number): number => { + const divisions = this.props.PanelWidth() / this.zoomScaling() / gridSpace + 3; + return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); + } + + @computed get grid() { + const gridSpace = this.chooseGridSpace(NumCast(this.layoutDoc["_backgroundGrid-spacing"], 50)); + const shiftX = (this.props.isAnnotationOverlay ? 0 : -this.panX() % gridSpace - gridSpace) * this.zoomScaling(); + const shiftY = (this.props.isAnnotationOverlay ? 0 : -this.panY() % gridSpace - gridSpace) * this.zoomScaling(); + const renderGridSpace = gridSpace * this.zoomScaling(); + const w = this.props.PanelWidth() + 2 * renderGridSpace; + const h = this.props.PanelHeight() + 2 * renderGridSpace; + return <canvas className="collectionFreeFormView-grid" width={w} height={h} style={{ transform: `translate(${shiftX}px, ${shiftY}px)` }} + ref={(el) => { + const ctx = el?.getContext('2d'); + if (ctx) { + const Cx = this.centeringShiftX() % renderGridSpace; + const Cy = this.centeringShiftY() % renderGridSpace; + ctx.lineWidth = Math.min(1, Math.max(0.5, this.zoomScaling())); + ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); + ctx.clearRect(0, 0, w, h); + if (ctx) { + ctx.strokeStyle = "rgba(0, 0, 0, 0.5)"; + ctx.beginPath(); + for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) { + ctx.moveTo(x, Cy - h); + ctx.lineTo(x, Cy + h); + } + for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { + ctx.moveTo(Cx - w, y); + ctx.lineTo(Cx + w, y); + } + ctx.stroke(); + } + } + }} />; + } @computed get marqueeView() { return <MarqueeView {...this.props} @@ -1436,6 +1488,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <div ref={this._marqueeRef}> + {this.layoutDoc["_backgroundGrid-show"] ? this.grid : (null)} <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} @@ -1452,10 +1505,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P </MarqueeView>; } + @computed get contentScaling() { if (this.props.annotationsKey && !this.props.forceScaling) return 0; - const nw = returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); - const nh = returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); + const nw = returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); + const nh = returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document)); const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; @@ -1624,7 +1678,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF } @computed get zoomProgressivize() { - return PresBox.Instance && PresBox.Instance.activeItem && PresBox.Instance.activeItem.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null); + return PresBox.Instance?.activeItem?.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null); } @computed get progressivize() { @@ -1655,6 +1709,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF </>; } + render() { // trace(); const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index fd4fa0c7e..cedeb1112 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -54,7 +54,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { <FontAwesomeIcon icon="font" size="lg" /> </button> </Tooltip>, - <Tooltip key="pinWithView" title={<><div className="dash-tooltip">Pin with selected view</div></>} placement="bottom"> + <Tooltip key="pinWithView" title={<><div className="dash-tooltip">Pin with selected region</div></>} placement="bottom"> <button className="antimodeMenu-button" onPointerDown={this.pinWithView}> diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 7c64fd429..7040b5e56 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -25,7 +25,7 @@ import "./MarqueeView.scss"; import React = require("react"); import { Id } from "../../../../fields/FieldSymbols"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; -import { PresMovement } from "../../nodes/PresBox"; +import { PresBox, PresMovement } from "../../nodes/PresBox"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -393,15 +393,24 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const selected = this.marqueeSelect(false); 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.context = curPres; - Doc.AddDocToList(curPres, "data", pinDoc); + pinDoc.title = doc.title + " - Slide"; + const presArray: Doc[] = PresBox.Instance?.sortArray(); + const size: number = PresBox.Instance?._selectedArray.size; + const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; + Doc.AddDocToList(curPres, "data", pinDoc, presSelected); if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; if (!DocumentManager.Instance.getDocumentView(curPres)) { 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 x = this.Bounds.left + this.Bounds.width / 2; const y = this.Bounds.top + this.Bounds.height / 2; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index b99bef15e..6e16137b5 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -213,10 +213,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu return this.props.addDocTab(doc, where); } getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { - const layoutTemp = this.props.DataDoc ? true : undefined; return <ContentFittingDocumentView Document={layout} - DataDoc={layout.resolvedDataDoc as Doc || (layoutTemp ? layout : undefined)} + DataDoc={layout.resolvedDataDoc as Doc} backgroundColor={this.props.backgroundColor} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -290,6 +289,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu return ( <div className={"collectionMulticolumnView_contents"} style={{ + width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`, + height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`, marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin), marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin) }} ref={this.createDashEventsTarget}> diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 1e0b023d7..b2ea87c06 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -33,7 +33,7 @@ $searchpanel-height: 32px; $mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc $docDecorations-zindex: 998; // then doc decorations appear over everything else $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right? -$COLLECTION_BORDER_WIDTH: 1; +$COLLECTION_BORDER_WIDTH: 0; $SCHEMA_DIVIDER_WIDTH: 4; $MINIMIZED_ICON_SIZE:25; $MAX_ROW_HEIGHT: 44px; diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 3b4912d3d..54b597f59 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -59,24 +59,17 @@ export class LinkMenu extends React.Component<Props> { } renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { - const linkItems: Array<JSX.Element> = []; - groups.forEach((group, groupType) => { - linkItems.push( - <LinkMenuGroup - key={groupType} - docView={this.props.docView} - sourceDoc={this.props.docView.props.Document} - group={group} - groupType={groupType} - showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)} - addDocTab={this.props.addDocTab} /> - ); - }); - - // if source doc has no links push message - if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>); - - return linkItems; + const linkItems = Array.from(groups.entries()).map(group => + <LinkMenuGroup + key={group[0]} + docView={this.props.docView} + sourceDoc={this.props.docView.props.Document} + group={group[1]} + groupType={group[0]} + showEditor={action(linkDoc => this._editingLink = linkDoc)} + addDocTab={this.props.addDocTab} />); + + return linkItems.length ? linkItems : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; } @computed diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index e76227ccf..e53655fd3 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,17 +1,12 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; -import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { Docs } from "../../documents/Documents"; -import { DragManager, SetupDrag } from "../../util/DragManager"; +import { Cast } from "../../../fields/Types"; import { LinkManager } from "../../util/LinkManager"; import { DocumentView } from "../nodes/DocumentView"; import './LinkMenu.scss'; -import { LinkMenuItem, StartLinkTargetsDrag } from "./LinkMenuItem"; +import { LinkMenuItem } from "./LinkMenuItem"; import React = require("react"); -import { Cast } from "../../../fields/Types"; interface LinkMenuGroupProps { sourceDoc: Doc; @@ -26,34 +21,8 @@ interface LinkMenuGroupProps { export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { private _drag = React.createRef<HTMLDivElement>(); - private _table = React.createRef<HTMLDivElement>(); private _menuRef = React.createRef<HTMLDivElement>(); - onLinkButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.addEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - document.addEventListener("pointerup", this.onLinkButtonUp); - } - - onLinkButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - e.stopPropagation(); - } - - onLinkButtonMoved = async (e: PointerEvent) => { - if (this._drag.current && (e.movementX > 1 || e.movementY > 1)) { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - - const targets = this.props.group.map(l => LinkManager.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[]; - StartLinkTargetsDrag(this._drag.current, this.props.docView, e.x, e.y, this.props.sourceDoc, targets); - } - e.stopPropagation(); - } - render() { const set = new Set<Doc>(this.props.group); const groupItems = Array.from(set.keys()).map(linkDoc => { @@ -75,11 +44,9 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { return ( <div className="linkMenu-group" ref={this._menuRef}> - {/* <div className="linkMenu-group-name"> - <p ref={this._drag} onPointerDown={this.onLinkButtonDown} - className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p> - {this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)} - </div> */} + <div className="linkMenu-group-name"> + <p ref={this._drag} className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p> + </div> <div className="linkMenu-group-wrapper"> {groupItems} diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 1ba724889..5c9123876 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -109,6 +109,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onLinkButtonUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); + DocumentView.followLinkClick(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false, false); e.stopPropagation(); } @@ -228,18 +229,17 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onPointerDown={this.onLinkButtonDown}> <div className="linkMenu-text"> - {source ? <p className="linkMenu-source-title"> - <b>Source: {source}</b></p> : null} + {source ? <p className="linkMenu-source-title"> <b>Source: {source}</b></p> : null} <div className="linkMenu-title-wrapper"> <div className="destination-icon-wrapper" > - <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" /></div> - <p className="linkMenu-destination-title" - onPointerDown={() => DocumentView.followLinkClick(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false, false)}> + <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" /> + </div> + <p className="linkMenu-destination-title"> {this.props.linkDoc.linksToAnnotation && Cast(this.props.destinationDoc.data, WebField)?.url.href === this.props.linkDoc.annotationUri ? "Annotation in" : ""} {title} </p> </div> - {this.props.linkDoc.description !== "" ? <p className="linkMenu-description"> - {StrCast(this.props.linkDoc.description)}</p> : null} </div> + {!this.props.linkDoc.description ? (null) : <p className="linkMenu-description">{StrCast(this.props.linkDoc.description)}</p>} + </div> <div className="linkMenu-item-buttons" ref={this._buttonRef} > diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 5a8ce4e14..c87239ee9 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -16,7 +16,7 @@ import { ComputedField } from "../../../fields/ScriptField"; import { listSpec } from "../../../fields/Schema"; import { DocumentType } from "../../documents/DocumentTypes"; import { Zoom, Fade, Flip, Rotate, Bounce, Roll, LightSpeed } from 'react-reveal'; -import { PresBox } from "./PresBox"; +import { PresBox, PresEffect } from "./PresBox"; import { InkingStroke } from "../InkingStroke"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @@ -55,8 +55,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @computed get freezeDimensions() { return this.props.FreezeDimensions; } @computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); } @computed get sizeProvider() { return this.props.sizeProvider?.(this.props.Document, this.props.replica); } - @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), NumCast(this.layoutDoc._nativeWidth, this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } - @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), NumCast(this.layoutDoc._nativeHeight, this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } + @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, undefined, this.freezeDimensions)); } + @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, undefined, this.freezeDimensions)); } public static getValues(doc: Doc, time: number) { const timecode = Math.round(time); @@ -212,23 +212,23 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF PanelHeight={this.panelHeight} />; if (PresBox.Instance && this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc) { const effectProps = { - left: this.layoutDoc.presEffectDirection === 'left', - right: this.layoutDoc.presEffectDirection === 'right', - top: this.layoutDoc.presEffectDirection === 'top', - bottom: this.layoutDoc.presEffectDirection === 'bottom', + left: this.layoutDoc.presEffectDirection === PresEffect.Left, + right: this.layoutDoc.presEffectDirection === PresEffect.Right, + top: this.layoutDoc.presEffectDirection === PresEffect.Top, + bottom: this.layoutDoc.presEffectDirection === PresEffect.Bottom, opposite: true, delay: this.layoutDoc.presTransition, // when: this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc, }; switch (this.layoutDoc.presEffect) { case "Zoom": return (<Zoom {...effectProps}>{node}</Zoom>); break; - case "Fade": return (<Fade {...effectProps}>{node}</Fade>); break; - case "Flip": return (<Flip {...effectProps}>{node}</Flip>); break; - case "Rotate": return (<Rotate {...effectProps}>{node}</Rotate>); break; - case "Bounce": return (<Bounce {...effectProps}>{node}</Bounce>); break; - case "Roll": return (<Roll {...effectProps}>{node}</Roll>); break; + case PresEffect.Fade: return (<Fade {...effectProps}>{node}</Fade>); break; + case PresEffect.Flip: return (<Flip {...effectProps}>{node}</Flip>); break; + case PresEffect.Rotate: return (<Rotate {...effectProps}>{node}</Rotate>); break; + case PresEffect.Bounce: return (<Bounce {...effectProps}>{node}</Bounce>); break; + case PresEffect.Roll: return (<Roll {...effectProps}>{node}</Roll>); break; case "LightSpeed": return (<LightSpeed {...effectProps}>{node}</LightSpeed>); break; - case "None": return node; break; + case PresEffect.None: return node; break; default: return node; break; } } else { diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index fcc9e50f5..4fb350b55 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -69,11 +69,11 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument SetActiveInkWidth(e.target.value); SelectionManager.SelectedDocuments().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeWidth = Number(e.target.value)); }} /> - <div> {ActiveInkBezierApprox() ?? 2}</div> + {/* <div> {ActiveInkBezierApprox() ?? 2}</div> <input type="range" defaultValue={ActiveInkBezierApprox() ?? 2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { SetActiveBezierApprox(e.target.value); SelectionManager.SelectedDocuments().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeBezier = e.target.value); - }} /> + }} /> */} <br /> <br /> </div> diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index acf6b1636..44cf5d046 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -13,6 +13,7 @@ left: 0; height: 100%; overflow: hidden; + transition: 200ms; .beforeBox-cont { height: 100%; @@ -26,6 +27,7 @@ width: 3px; display: inline-block; background: white; + transition: 200ms; .slide-handle { position: absolute; diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 2af5b3182..0ba53dee6 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -51,9 +51,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { // on click, animate slider movement to the targetWidth - this._animating = "all 1s"; + this._animating = "all 200ms"; this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth(); - setTimeout(action(() => this._animating = ""), 1000); + setTimeout(action(() => this._animating = ""), 200); }), false); } @@ -102,12 +102,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C return ( <div className={`comparisonBox${this.active() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}> - {displayBox("after", 1, this.props.PanelWidth() - 5)} + {displayBox("after", 1, this.props.PanelWidth() - 3)} <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}> - {displayBox("before", 0, 5)} + {displayBox("before", 0, 0)} </div> - <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)` }} + <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)`, cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? "e-resize" : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? "w-resize" : undefined }} onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index 09051da78..0c52b9044 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -22,12 +22,8 @@ export class ContentFittingDocumentView extends React.Component<DocumentViewProp } @computed get freezeDimensions() { return this.props.FreezeDimensions; } - nativeWidth = () => returnVal(this.props.NativeWidth?.(), - NumCast(this.layoutDoc?._nativeWidth || this.props.DataDoc?.[Doc.LayoutFieldKey(this.layoutDoc) + "-nativeWidth"], - (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[WidthSym]() : this.props.PanelWidth()))) - nativeHeight = () => returnVal(this.props.NativeHeight?.(), - NumCast(this.layoutDoc?._nativeHeight || this.props.DataDoc?.[Doc.LayoutFieldKey(this.layoutDoc) + "-nativeHeight"], - (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[HeightSym]() : this.props.PanelHeight()))) + nativeWidth = () => returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.freezeDimensions) || this.props.PanelWidth()); + nativeHeight = () => returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.freezeDimensions) || this.props.PanelHeight()); @computed get scaling() { const wscale = this.props.PanelWidth() / this.nativeWidth(); const hscale = this.props.PanelHeight() / this.nativeHeight(); @@ -42,7 +38,13 @@ export class ContentFittingDocumentView extends React.Component<DocumentViewProp private PanelHeight = () => this.panelHeight; @computed get panelWidth() { return this.nativeWidth() && !this.props.Document._fitWidth ? this.nativeWidth() * this.contentScaling() : this.props.PanelWidth(); } - @computed get panelHeight() { return this.nativeHeight() && !this.props.Document._fitWidth ? this.nativeHeight() * this.contentScaling() : this.props.PanelHeight(); } + @computed get panelHeight() { + if (this.nativeHeight()) { + if (!this.props.Document._fitWidth) return this.nativeHeight() * this.contentScaling(); + else return this.panelWidth / Doc.NativeAspect(this.layoutDoc, this.props.DataDoc, this.freezeDimensions) || 1; + } + return this.props.PanelHeight(); + } @computed get childXf() { return this.props.DataDoc ? 1 : 1 / this.contentScaling(); } // this is intended to detect when a document is being rendered inside itself as part of a template, but not as a leaf node where nativeWidth & height would apply. private getTransform = () => this.props.dontCenter ? diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 9328fb96b..735aa669f 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -1,6 +1,11 @@ @import "../globalCssVariables.scss"; +.documentLinksButton-cont { + min-width: 20; + min-height: 20; + position: absolute; +} .documentLinksButton, .documentLinksButton-endLink, .documentLinksButton-startLink { @@ -29,6 +34,7 @@ .documentLinksButton { background-color: black; + font-weight: bold; &:hover { background: $main-accent; diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index ddfb3cc34..95c007175 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -3,7 +3,6 @@ import { Tooltip } from "@material-ui/core"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; -import { DocumentType } from "../../documents/DocumentTypes"; import { emptyFunction, setupMoveUpEvents, returnFalse, Utils, emptyPath } from "../../../Utils"; import { TraceMobx } from "../../../fields/util"; import { DocUtils, Docs } from "../../documents/Documents"; @@ -39,6 +38,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @observable public static StartLinkView: DocumentView | undefined; @observable public static AnnotationId: string | undefined; @observable public static AnnotationUri: string | undefined; + @observable public static EditLink: DocumentView | undefined; @observable public static invisibleWebDoc: Opt<Doc>; public static invisibleWebRef = React.createRef<HTMLDivElement>(); @@ -209,94 +209,75 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } })); - @observable - public static EditLink: DocumentView | undefined; @action clearLinks() { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } - @computed - get linkButton() { - TraceMobx(); - const links = DocUtils.FilterDocs(Array.from(new Set<Doc>(this.props.links)), this.props.View.props.docFilters(), []); - - const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link"; - const buttonTitle = "Tap to view links"; - const title = this.props.InMenu ? menuTitle : buttonTitle; - - - const startLink = <img - style={{ width: "11px", height: "11px" }} - id={"startLink-icon"} - src={`/assets/${"startLink.png"}`} />; - - const endLink = <img - style={{ width: "14px", height: "9px" }} - id={"endLink-icon"} - src={`/assets/${"endLink.png"}`} />; + @computed get filteredLinks() { + return DocUtils.FilterDocs(Array.from(new Set<Doc>(this.props.links)), this.props.View.props.docFilters(), []); + } - const link = <img - style={{ width: "22px", height: "16px" }} - id={"link-icon"} - src={`/assets/${"link.png"}`} />; + @computed get linkButtonInner() { + const btnDim = this.props.InMenu ? "20px" : "30px"; + const link = <img style={{ width: "22px", height: "16px" }} src={`/assets/${"link.png"}`} />; - const linkButton = <div className="documentLinksButton-cont" ref={this._linkButton} style={{ - minWidth: 20, minHeight: 20, position: "absolute", - left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] - }}> - <div className={"documentLinksButton"} style={{ - backgroundColor: this.props.InMenu ? "" : "#add8e6", - color: this.props.InMenu ? "white" : "black", - width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", fontWeight: "bold" - }} + return <div className="documentLinksButton-cont" ref={this._linkButton} + style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }} + > + <div className={"documentLinksButton"} onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick} - // onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} - // onPointerEnter={action(e => links.length && (LinkDocPreview.LinkInfo = { - // addDocTab: this.props.View.props.addDocTab, - // linkSrc: this.props.View.props.Document, - // linkDoc: links[0], - // Location: [e.clientX, e.clientY + 20] - // }))} - > - - {/* {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : - <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} */} - - {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : - link : Array.from(links).length} - + style={{ + backgroundColor: this.props.InMenu ? "" : "#add8e6", + color: this.props.InMenu ? "white" : "black", + width: btnDim, + height: btnDim, + }} > + {this.props.InMenu ? + this.props.StartLink ? + <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> + : link + : Array.from(this.filteredLinks).length} </div> {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? <div className={"documentLinksButton-endLink"} style={{ - width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", + width: btnDim, height: btnDim, backgroundColor: DocumentLinksButton.StartLink ? "" : "grey", opacity: DocumentLinksButton.StartLink ? "" : "50%", border: DocumentLinksButton.StartLink ? "" : "none", cursor: DocumentLinksButton.StartLink ? "pointer" : "default" }} - onPointerDown={DocumentLinksButton.StartLink ? this.completeLink : emptyFunction} - onClick={e => DocumentLinksButton.StartLink ? DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View) : emptyFunction} /> : (null) + onPointerDown={DocumentLinksButton.StartLink && this.completeLink} + onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)} /> + : (null) } - { - DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} - style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} - onPointerDown={this.clearLinks} onClick={this.clearLinks} - /> : (null) + {DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? + <div className={"documentLinksButton-startLink"} onPointerDown={this.clearLinks} onClick={this.clearLinks} style={{ width: btnDim, height: btnDim }} /> + : (null) } </div >; + } + + @computed get linkButton() { + TraceMobx(); + + const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link"; + const buttonTitle = "Tap to view links"; + const title = this.props.InMenu ? menuTitle : buttonTitle; - return (!Array.from(links).length) && !this.props.AlwaysOn ? (null) : + return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? (null) : this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> - {linkButton} - </Tooltip> : !!!DocumentLinksButton.EditLink && !this.props.InMenu ? + {this.linkButtonInner} + </Tooltip> + : + !DocumentLinksButton.EditLink && !this.props.InMenu ? <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> - {linkButton} - </Tooltip> : - linkButton; + {this.linkButtonInner} + </Tooltip> + : this.linkButtonInner; } render() { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e980322d5..77f63b457 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -42,7 +42,8 @@ import { RadialMenu } from './RadialMenu'; import { TaskCompletionBox } from './TaskCompletedBox'; import React = require("react"); -export type DocFocusFunc = () => boolean; +export type DocAfterFocusFunc = (notFocused: boolean) => boolean; +export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => void; export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; @@ -82,7 +83,7 @@ export interface DocumentViewProps { PanelHeight: () => number; pointerEvents?: string; contentsPointerEvents?: string; - focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: DocFocusFunc) => void; + focus: DocFocusFunc; parentActive: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; @@ -129,8 +130,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @computed get topMost() { return this.props.renderDepth === 0; } @computed get freezeDimensions() { return this.props.FreezeDimensions; } - @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), NumCast(this.layoutDoc[(this.props.DataDoc ? this.LayoutFieldKey + "-" : "_") + "nativeWidth"], (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0))); } - @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), NumCast(this.layoutDoc[(this.props.DataDoc ? this.LayoutFieldKey + "-" : "_") + "nativeHeight"], (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0))); } + @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.dataDoc, this.freezeDimensions)); } + @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.dataDoc, this.freezeDimensions)); } @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } @@ -325,17 +326,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey }, console.log); - func(); + undoBatch(func)(); } else if (!Doc.IsSystem(this.props.Document)) { if (this.props.Document.type === DocumentType.INK) { InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true); } else { UndoManager.RunInBatch(() => { - let fullScreenDoc = this.props.Document; - if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) { - fullScreenDoc = Doc.MakeAlias(this.props.Document); - fullScreenDoc.layoutKey = "layout_fullScreen"; - } + const fullScreenDoc = Cast(this.props.Document._fullScreenView, Doc, null) || this.props.Document; this.props.addDocTab(fullScreenDoc, "add"); }, "double tap"); SelectionManager.DeselectAll(); @@ -385,7 +382,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); public static followLinkClick = async (linkDoc: Opt<Doc>, sourceDoc: Doc, docView: { - focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: DocFocusFunc) => void, + focus: DocFocusFunc, addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean, ContainingCollectionDoc?: Doc }, shiftKey: boolean, altKey: boolean) => { @@ -423,7 +420,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._downX = touch.clientX; this._downY = touch.clientY; if (!e.nativeEvent.cancelBubble) { - if ((this.active || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) e.stopPropagation(); + if ((this.active || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc.lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) e.stopPropagation(); this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); @@ -438,7 +435,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (e.cancelBubble && this.active) { this.removeMoveListeners(); } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc.lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { const touch = me.touchEvent.changedTouches.item(0); if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) { @@ -494,8 +491,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = Document(this.props.Document); const layoutDoc = Document(Doc.Layout(this.props.Document)); - let nwidth = layoutDoc._nativeWidth || 0; - let nheight = layoutDoc._nativeHeight || 0; + let nwidth = Doc.NativeWidth(layoutDoc); + let nheight = Doc.NativeHeight(layoutDoc); const width = (layoutDoc._width || 0); const height = (layoutDoc._height || (nheight / nwidth * width)); const scale = this.props.ScreenToLocalTransform().Scale * this.props.ContentScaling(); @@ -505,13 +502,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu doc.y = (doc.y || 0) + dY * (actualdH - height); const fixedAspect = e.ctrlKey || (nwidth && nheight); if (fixedAspect && (!nwidth || !nheight)) { - layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; - layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; + Doc.SetNativeWidth(layoutDoc, nwidth = layoutDoc._width || 0); + Doc.SetNativeHeight(layoutDoc, nheight = layoutDoc._height || 0); } if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH)) { if (!fixedAspect) { - layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0); + Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc)); } layoutDoc._width = actualdW; if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; @@ -519,7 +516,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } else { if (!fixedAspect) { - layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0); + Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc)); } layoutDoc._height = actualdH; if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; @@ -554,7 +551,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if ((this.active || this.layoutDoc.onDragStart) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && - !this.layoutDoc.inOverlay) { + !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { e.stopPropagation(); if (SelectionManager.IsSelected(this, true) && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it } @@ -573,7 +570,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart) && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart) && !this.layoutDoc.lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener("pointermove", this.onPointerMove); @@ -754,8 +751,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.bringToFront(this.props.Document, true); const wid = this.Document[WidthSym](); // change the nativewidth and height if the background is to be a collection that aggregates stuff that is added to it. const hgt = this.Document[HeightSym](); - this.props.Document[DataSym][this.LayoutFieldKey + "-nativeWidth"] = wid; - this.props.Document[DataSym][this.LayoutFieldKey + "-nativeHeight"] = hgt; + Doc.SetNativeWidth(this.props.Document[DataSym], wid); + Doc.SetNativeHeight(this.props.Document[DataSym], hgt); } } @@ -839,8 +836,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" }); onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" }); onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" }); + !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); } - !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); } const funcs: ContextMenuProps[] = []; @@ -889,7 +886,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e?.stopPropagation(); // DocumentViews should stop propagation of this event } cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); - !this.isSelected(true) && SelectionManager.SelectDoc(this, false); + !this.isSelected(true) && setTimeout(() => SelectionManager.SelectDoc(this, false), 300); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. }); } @@ -907,7 +904,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const excluded = ["PresBox", /* "FormattedTextBox", */ "FontIconBox"]; // bcz: shifting the title for texst causes problems with collaborative use when some people see titles, and others don't return !excluded.includes(StrCast(this.layoutDoc.layout)); } - chromeHeight = () => this.showOverlappingTitle ? 1 : 25; + chromeHeight = () => this.showOverlappingTitle ? 0 : 25; @computed get finalLayoutKey() { if (typeof this.props.layoutKey === "string") { @@ -920,7 +917,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; } childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); - @computed.struct get linkOffset() { return this.topMost ? [0, undefined, undefined, 10] : [-15, undefined, undefined, undefined]; } + @computed.struct get linkOffset() { return this.topMost ? [0, undefined, undefined, 10] : [-15, undefined, undefined, -20]; } @computed get contents() { TraceMobx(); return (<div className="documentView-contentsView" style={{ pointerEvents: this.props.contentsPointerEvents as any, borderRadius: "inherit", width: "100%", height: "100%" }}> @@ -990,7 +987,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu anchorPanelHeight = () => this.props.PanelHeight() || 1; @computed get directLinks() { TraceMobx(); return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } - @computed get allLinks() { TraceMobx(); return DocListCast(this.Document.links); } + @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } @computed get allAnchors() { TraceMobx(); if (this.props.LayoutTemplateString?.includes("LinkAnchorBox")) return null; @@ -1144,19 +1141,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu style={{ transformOrigin: this._animateScalingTo ? "center center" : undefined, transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : this._animateScalingTo < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out", + transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform 0.5s ease-${this._animateScalingTo < 1 ? "in" : "out"}`, pointerEvents: this.ignorePointerEvents ? "none" : undefined, color: StrCast(this.layoutDoc.color, "inherit"), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", - border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - boxShadow: this.Document.isLinkButton && !this.props.dontRegisterView && this.props.forcedBackgroundColor?.(this.Document) !== "transparent" ? - StrCast(this.props.Document._linkButtonShadow, "lightblue 0em 0em 1em") : - this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : - undefined, + border: highlighting && borderRounding && highlightStyles[fullDegree] === "dashed" ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, + boxShadow: highlighting && borderRounding && highlightStyles[fullDegree] !== "dashed" ? `0 0 0 ${localScale}px ${highlightColors[fullDegree]}` : + this.Document.isLinkButton && !this.props.dontRegisterView && this.props.forcedBackgroundColor?.(this.Document) !== "transparent" ? + StrCast(this.layoutDoc._linkButtonShadow, "lightblue 0em 0em 1em") : + this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : + undefined, background: finalColor, opacity: finalOpacity, fontFamily: StrCast(this.Document._fontFamily, "inherit"), - fontSize: Cast(this.Document._fontSize, "string", null), + fontSize: !this.props.treeViewDoc ? Cast(this.Document._fontSize, "string", null) : undefined, }}> {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <> {this.innards} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 3a5b27b21..79947c023 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -11,6 +11,7 @@ import { CollectionView } from "../collections/CollectionView"; import { AudioBox } from "./AudioBox"; import { VideoBox } from "./VideoBox"; import { dropActionType } from "../../util/DragManager"; +import { DocAfterFocusFunc, DocFocusFunc } from "./DocumentView"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -47,7 +48,7 @@ export interface FieldViewProps { whenActiveChanged: (isActive: boolean) => void; LayoutTemplateString?: string; dontRegisterView?: boolean; - focus: (doc: Doc) => void; + focus: DocFocusFunc; presMultiSelect?: (doc: Doc) => void; //added for selecting multiple documents in a presentation ignoreAutoHeight?: boolean; PanelWidth: () => number; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index bc38a17eb..88dc3b241 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -76,14 +76,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD componentDidMount() { this._pathDisposer = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), - ({ nativeSize, width }) => { - if (this.props.NativeWidth?.() !== 0 || !this.layoutDoc._height) { - this.layoutDoc._nativeWidth = nativeSize.nativeWidth; - this.layoutDoc._nativeHeight = nativeSize.nativeHeight; - this.layoutDoc._nativeOrientation = nativeSize.nativeOrientation; + action(({ nativeSize, width }) => { + if (!this.layoutDoc._height) { this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth; } - }, + }), { fireImmediately: true }); } @@ -106,8 +103,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const targetDoc = layoutDoc[DataSym]; if (targetDoc[targetField] instanceof ImageField) { this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); - this.dataDoc[this.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]); - this.dataDoc[this.fieldKey + "-nativeHeight"] = NumCast(targetDoc[targetField + "-nativeHeight"]); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc)); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeHeight(targetDoc)); e.stopPropagation(); } } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 867be9735..d47942bd9 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -77,19 +77,16 @@ export class LinkDocPreview extends React.Component<Props> { this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { _fitWidth: true, title: this.props.href, _width: 200, _height: 400, useCors: true }), "add:right"); } } - width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)) - 16; - height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)) - 16; + width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); + height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); @computed get targetDocView() { return !this._targetDoc ? - <div style={{ - pointerEvents: "all", maxWidth: 225, maxHeight: 225, width: "100%", height: "100%", - overflow: "hidden" - }}> + <div style={{ pointerEvents: "all", maxWidth: 225, maxHeight: 225, width: "100%", height: "100%", overflow: "hidden" }}> <div style={{ width: "100%", height: "100%", textOverflow: "ellipsis", }} onPointerDown={this.pointerDown}> {this._toolTipText} </div> - </div> : - + </div> + : <ContentFittingDocumentView Document={this._targetDoc} LibraryPath={emptyPath} @@ -109,8 +106,8 @@ export class LinkDocPreview extends React.Component<Props> { ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} renderDepth={-1} - PanelWidth={this.width} //Math.min(350, NumCast(target._width, 350))} - PanelHeight={this.height} //Math.min(250, NumCast(target._height, 250))} + PanelWidth={this.width} + PanelHeight={this.height} focus={emptyFunction} whenActiveChanged={returnFalse} bringToFront={returnFalse} diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index e2bdd98cc..16cc9d27e 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -208,6 +208,7 @@ pointer-events: none; .pdfViewerDash-text { .textLayer { + display: none; span { user-select: none; } @@ -228,6 +229,7 @@ z-index: -1; .pdfViewerDash-text { .textLayer { + display: contents; span { user-select: text; } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 79e00eed7..42b24f6f6 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -50,8 +50,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum constructor(props: any) { super(props); this._initialScale = this.props.ScreenToLocalTransform().Scale; - const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927)); - const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200)); + const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; + const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); if (pdfUrl) { @@ -97,8 +97,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum loaded = (nw: number, nh: number, np: number) => { this.dataDoc[this.props.fieldKey + "-numPages"] = np; - this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = Math.max(NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]), nw * 96 / 72); - this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = nh * 96 / 72; + Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), nw * 96 / 72)); + Doc.SetNativeHeight(this.dataDoc, nh * 96 / 72); + this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1); !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } @@ -254,7 +255,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}> <div className="pdfBox-background"></div> - <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} + <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} addDocTab={this.props.addDocTab} focus={this.props.focus} searchFilterDocs={this.props.searchFilterDocs} diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss index 9a8b861ba..de2aee8fa 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/PresBox.scss @@ -56,11 +56,14 @@ $light-background: #ececec; letter-spacing: 0; display: flex; align-items: center; + justify-content: center; transition: 0.5s; } .toolbar-button.active { color: $light-blue; + background-color: white; + border-radius: 100%; } .toolbar-transitionButtons { @@ -128,7 +131,6 @@ $light-background: #ececec; position: relative; display: inline; font-family: Roboto; - color: black; z-index: 100; transition: 0.7s; @@ -509,6 +511,7 @@ $light-background: #ececec; background-color: $light-background; border: solid 1px rgba(0, 0, 0, 0.5); display: flex; + color: black; margin-top: 5px; margin-bottom: 5px; border-radius: 5px; @@ -569,6 +572,7 @@ $light-background: #ececec; border-radius: 5px; font-size: 10; height: 25; + color: black; padding-left: 5px; align-items: center; margin-top: 5px; @@ -997,6 +1001,7 @@ $light-background: #ececec; transition: all 0.2s; .presPanel-button-text { + cursor: pointer; display: flex; height: 20; width: max-content; @@ -1049,6 +1054,7 @@ $light-background: #ececec; .presPanel-button:hover { background-color: #5a5a5a; + transform: scale(1.2); } .presPanel-button-text:hover { diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 8e3679452..683cb938a 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable, runInAction, ObservableMap, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; import { Doc, DocCastAsync, DocListCast, DocListCastAsync } from "../../../fields/Doc"; @@ -11,7 +11,7 @@ import { List } from "../../../fields/List"; import { PrefetchProxy } from "../../../fields/Proxy"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; import { returnFalse, returnOne, returnZero } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -19,7 +19,7 @@ import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { DocumentManager } from "../../util/DocumentManager"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; -import { undoBatch } from "../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; import { TabDocView } from "../collections/TabDocView"; @@ -28,7 +28,7 @@ import { AudioBox } from "./AudioBox"; import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import "./PresBox.scss"; -import { VideoBox } from "./VideoBox"; +import Color = require("color"); export enum PresMovement { Zoom = "zoom", @@ -38,12 +38,30 @@ export enum PresMovement { } export enum PresEffect { - Fade = "fade", - Flip = "flip", - Rotate = "rotate", - Bounce = "bounce", - Roll = "roll", - None = "none", + Fade = "Fade in", + Flip = "Flip", + Rotate = "Rotate", + Bounce = "Bounce", + Roll = "Roll", + None = "None", + Left = "left", + Right = "right", + Center = "center", + Top = "top", + Bottom = "bottom" +} + +enum PresStatus { + Autoplay = "auto", + Manual = "manual", + Edit = "edit" +} + +enum PresColor { + LightBlue = "#AEDDF8", + DarkBlue = "#5B9FDD", + LightBackground = "#ececec", + SlideBackground = "#d5dce2", } type PresBoxSchema = makeInterface<[typeof documentSchema]>; @@ -53,35 +71,53 @@ const PresBoxDocument = makeInterface(documentSchema); export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>(PresBoxDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); } - public static Instance: PresBox; + @observable public static Instance: PresBox; @observable _isChildActive = false; @observable _moveOnFromAudio: boolean = true; @observable _presTimer!: NodeJS.Timeout; + @observable _presKeyEventsActive: boolean = false; - @observable _selectedArray: Doc[] = []; - @observable _sortedSelectedArray: Doc[] = []; + @observable _selectedArray: ObservableMap = new ObservableMap<Doc, any>(); @observable _eleArray: HTMLElement[] = []; @observable _dragArray: HTMLElement[] = []; + @observable _pathBoolean: boolean = false; + @observable _expandBoolean: boolean = false; + private _disposers: { [name: string]: IReactionDisposer } = {}; @observable private transitionTools: boolean = false; @observable private newDocumentTools: boolean = false; @observable private progressivizeTools: boolean = false; - @observable private presentTools: boolean = false; - @observable private pathBoolean: boolean = false; @observable private openMovementDropdown: boolean = false; @observable private openEffectDropdown: boolean = false; + @observable private presentTools: boolean = false; @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } + @computed get tagDocs() { + const tagDocs: Doc[] = []; + for (const doc of this.childDocs) { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + tagDocs.push(tagDoc); + } + return tagDocs; + } @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); } - @computed get activeItem() { return Cast(this.childDocs[this.itemIndex], Doc, null); } + @computed get activeItem() { return Cast(this.childDocs[NumCast(this.rootDoc._itemIndex)], Doc, null); } @computed get targetDoc() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); } + @computed get scrollable(): boolean { + if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true; + else return false; + } + @computed get panable(): boolean { + if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; + else return false; + } @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); } constructor(props: any) { super(props); - if (Doc.UserDoc().activePresentation = this.rootDoc) PresBox.Instance = this; + if (Doc.UserDoc().activePresentation = this.rootDoc) runInAction(() => PresBox.Instance = this); if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations. Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ - title: "pres element template", backgroundColor: "transparent", _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data" + title: "pres element template", type: DocumentType.PRESELEMENT, backgroundColor: "transparent", _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data" })); // this script will be called by each presElement to get rendering-specific info that the PresBox knows about but which isn't written to the PresElement // this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent @@ -94,139 +130,145 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get selectedDocumentView() { if (SelectionManager.SelectedDocuments().length) return SelectionManager.SelectedDocuments()[0]; - if (PresBox.Instance && PresBox.Instance._selectedArray) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); - return undefined; + if (this._selectedArray.size) return DocumentManager.Instance.getDocumentView(this.rootDoc); } @computed get isPres(): boolean { - document.removeEventListener("keydown", this.keyEvents, true); + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); if (this.selectedDoc?.type === DocumentType.PRES) { - document.addEventListener("keydown", this.keyEvents, true); + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); + document.addEventListener("keydown", PresBox.keyEventsWrapper, true); return true; } return false; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @action componentWillUnmount() { - document.removeEventListener("keydown", this.keyEvents, true); + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); + this._presKeyEventsActive = false; + this.resetPresentation(); // Turn of progressivize editors this.turnOffEdit(true); + Object.values(this._disposers).forEach(disposer => disposer?.()); } - componentDidMount = async () => { + @action + componentDidMount() { this.rootDoc.presBox = this.rootDoc; this.rootDoc._forceRenderEngine = "timeline"; this.rootDoc._replacedChrome = "replaced"; - this.layoutDoc.presStatus = "edit"; + this.layoutDoc.presStatus = PresStatus.Edit; this.layoutDoc._gridGap = 0; this.layoutDoc._yMargin = 0; this.turnOffEdit(true); DocListCastAsync((Doc.UserDoc().myPresentations as Doc).data).then(pres => !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc)); + this._disposers.selection = reaction(() => SelectionManager.SelectedDocuments(), + views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()); } - updateCurrentPresentation = () => { - Doc.UserDoc().activePresentation = this.rootDoc; + @action + updateCurrentPresentation = (pres?: Doc) => { + if (pres) Doc.UserDoc().activePresentation = pres; + else Doc.UserDoc().activePresentation = this.rootDoc; + document.removeEventListener("keydown", PresBox.keyEventsWrapper, true); + document.addEventListener("keydown", PresBox.keyEventsWrapper, true); + this._presKeyEventsActive = true; PresBox.Instance = this; } - /** - * Called when the user moves to the next slide in the presentation trail. - */ - @undoBatch + // There are still other internal frames and should go through all frames before going to next slide + nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => { + const currentFrame = Cast(targetDoc?._currentFrame, "number", null); + const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + targetDoc._viewTransition = "all 1s"; + setTimeout(() => targetDoc._viewTransition = undefined, 1010); + this.nextKeyframe(targetDoc, activeItem); + if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc); + else targetDoc.editing = true; + } + + // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played + nextAudioVideo = (targetDoc: Doc, activeItem: Doc) => { + if (targetDoc.type === DocumentType.AUDIO) AudioBox.Instance.playFrom(NumCast(activeItem.presStartTime)); + // if (targetDoc.type === DocumentType.VID) { VideoBox.Instance.Play() }; + activeItem.playNow = false; + } + + // No more frames in current doc and next slide is defined, therefore move to next slide + nextSlide = (targetDoc: Doc, activeNext: Doc) => { + const nextSelected = this.itemIndex + 1; + this.gotoDocument(nextSelected); + + // const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); + // If next slide is audio / video 'Play automatically' then the next slide should be played + // if (activeNext && (targetNext.type === DocumentType.AUDIO || targetNext.type === DocumentType.VID) && activeNext.playAuto) { + // console.log('play next automatically'); + // if (targetNext.type === DocumentType.AUDIO) AudioBox.Instance.playFrom(NumCast(activeNext.presStartTime)); + // // if (targetNext.type === DocumentType.VID) { VideoBox.Instance.Play() }; + // } else if (targetNext.type === DocumentType.AUDIO || targetNext.type === DocumentType.VID) { activeNext.playNow = true; console.log('play next after it is navigated to'); } + } + + // Called when the user activates 'next' - to move to the next part of the pres. trail @action next = () => { - this.updateCurrentPresentation(); const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null); const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; - const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); - const currentFrame = Cast(targetDoc._currentFrame, "number", null); - const lastFrame = Cast(targetDoc.lastFrame, "number", null); - const curFrame = NumCast(targetDoc._currentFrame); + const lastFrame = Cast(targetDoc?.lastFrame, "number", null); + const curFrame = NumCast(targetDoc?._currentFrame); let internalFrames: boolean = false; if (activeItem.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true; - // Case 1: There are still other frames and should go through all frames before going to next slide if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) { - targetDoc._viewTransition = "all 1s"; - setTimeout(() => targetDoc._viewTransition = undefined, 1010); - // targetDoc._currentFrame = curFrame + 1; - this.nextKeyframe(targetDoc, activeItem); - if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc); - else targetDoc.editing = true; - // if (activeItem.zoomProgressivize) this.zoomProgressivizeNext(targetDoc); - // Case 2: 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played - } else if ((targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && !activeItem.playAuto && activeItem.playNow && this.layoutDoc.presStatus !== 'auto') { - if (targetDoc.type === DocumentType.AUDIO) AudioBox.Instance.playFrom(NumCast(activeItem.presStartTime)); - // if (targetDoc.type === DocumentType.VID) { VideoBox.Instance.Play() }; - activeItem.playNow = false; - // Case 3: No more frames in current doc and next slide is defined, therefore move to next slide + // Case 1: There are still other frames and should go through all frames before going to next slide + this.nextInternalFrame(targetDoc, activeItem); } else if (this.childDocs[this.itemIndex + 1] !== undefined) { - if (activeNext.presPinView) setTimeout(() => this.selectPres(), 0); - else this.selectPres(); - const nextSelected = this.itemIndex + 1; - if (targetDoc.type === DocumentType.AUDIO) { if (AudioBox.Instance._ele) AudioBox.Instance.pause(); } - // if (targetDoc.type === DocumentType.VID) { if (AudioBox.Instance._ele) VideoBox.Instance.Pause(); } - const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); - // If next slide is audio / video 'Play automatically' then the next slide should be played - if (activeNext && (targetNext.type === DocumentType.AUDIO || targetNext.type === DocumentType.VID) && activeNext.playAuto) { - console.log('play next automatically'); - if (targetNext.type === DocumentType.AUDIO) AudioBox.Instance.playFrom(NumCast(activeNext.presStartTime)); - // if (targetNext.type === DocumentType.VID) { VideoBox.Instance.Play() }; - } else if (targetNext.type === DocumentType.AUDIO || targetNext.type === DocumentType.VID) { activeNext.playNow = true; console.log('play next after it is navigated to'); } - this.gotoDocument(nextSelected, this.itemIndex); + // Case 3: No more frames in current doc and next slide is defined, therefore move to next slide + this.nextSlide(targetDoc, activeNext); } else if (this.childDocs[this.itemIndex + 1] === undefined && this.layoutDoc.presLoop) { - this.gotoDocument(0, this.itemIndex); + // Case 4: Last slide and presLoop is toggled ON + this.gotoDocument(0); } + // else if ((targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && !activeItem.playAuto && activeItem.playNow && this.layoutDoc.presStatus !== PresStatus.Autoplay) { + // // Case 2: 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played + // this.nextAudioVideo(targetDoc, activeItem); + // } } - /** - * Called when the user moves back - * Design choice: If there are frames within the presentation, moving back will not - * got back through the frames but instead directly to the next point in the presentation. - */ - @undoBatch + // Called when the user activates 'back' - to move to the previous part of the pres. trail @action back = () => { - this.updateCurrentPresentation(); const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null); const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null); const lastFrame = Cast(targetDoc.lastFrame, "number", null); const curFrame = NumCast(targetDoc._currentFrame); - if (prevItem.presPinView) setTimeout(() => this.selectPres(), 0); - else this.selectPres(); + let prevSelected = this.itemIndex; if (lastFrame !== undefined && curFrame >= 1) { + // Case 1: There are still other frames and should go through all frames before going to previous slide this.prevKeyframe(targetDoc, activeItem); - } else if (activeItem) { - let prevSelected = this.itemIndex; + } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) { + // Case 2: There are no other frames so it should go to the previous slide prevSelected = Math.max(0, prevSelected - 1); - this.gotoDocument(prevSelected, this.itemIndex); + this.gotoDocument(prevSelected); if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame); + } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) { + // Case 3: Pres loop is on so it should go to the last slide + this.gotoDocument(this.childDocs.length - 1); } } //The function that is called when a document is clicked or reached through next or back. //it'll also execute the necessary actions if presentation is playing. - public gotoDocument = action((index: number, fromDoc: number) => { - this.updateCurrentPresentation(); + public gotoDocument = action((index: number) => { Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; const activeItem: Doc = this.activeItem; const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); - if (activeItem.presPinView) { - const bestTarget = DocumentManager.Instance.getFirstDocumentView(presTargetDoc)?.props.Document; - bestTarget && runInAction(() => { - if (activeItem.presMovement === PresMovement.Jump) { - bestTarget._viewTransition = '0s'; - } else { - bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 1s'; - setTimeout(() => bestTarget._viewTransition = undefined, activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 1010); - } - }); - } else { + if (presTargetDoc) { presTargetDoc && runInAction(() => { if (activeItem.presMovement === PresMovement.Jump) presTargetDoc.focusSpeed = 0; else presTargetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500; @@ -236,13 +278,35 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (presTargetDoc?.lastFrame !== undefined) { presTargetDoc._currentFrame = 0; } - this._selectedArray = [this.childDocs[index]]; //Update selected array - //Handles movement to element - if (this.layoutDoc._viewType === "stacking") this.navigateToElement(this.childDocs[index]); + this._selectedArray.clear(); + this.childDocs[index] && this._selectedArray.set(this.childDocs[index], undefined); //Update selected array + if (this.layoutDoc._viewType === "stacking") this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list this.onHideDocument(); //Handles hide after/before } }); + _navTimer!: NodeJS.Timeout; + navigateToView = (targetDoc: Doc, activeItem: Doc) => { + clearTimeout(this._navTimer); + const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; + bestTarget && runInAction(() => { + if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { + bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; + bestTarget._scrollY = activeItem.presPinViewScroll; + } else if (bestTarget.type === DocumentType.COMPARISON) { + bestTarget._clipWidth = activeItem.presPinClipWidth; + } else if (bestTarget.type === DocumentType.VID) { + bestTarget._currentTimecode = activeItem.presPinTimecode; + } else { + bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; + bestTarget._panX = activeItem.presPinViewX; + bestTarget._panY = activeItem.presPinViewY; + bestTarget._viewScale = activeItem.presPinViewScale; + } + this._navTimer = setTimeout(() => bestTarget._viewTransition = undefined, activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510); + }); + } + /** * This method makes sure that cursor navigates to the element that * has the option open and last in the group. @@ -254,68 +318,55 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> navigateToElement = async (curDoc: Doc) => { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; - const srcContext = await DocCastAsync(targetDoc.context); + const srcContext = Cast(targetDoc.context, Doc, null); const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); - const collectionDocView = presCollection ? await DocumentManager.Instance.getDocumentView(presCollection) : undefined; + const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined; + const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc); + const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext); this.turnOffEdit(); - if (this.itemIndex >= 0) { - if (srcContext && targetDoc) { - this.layoutDoc.presCollection = srcContext; - } else if (targetDoc) this.layoutDoc.presCollection = targetDoc; + // Handles the setting of presCollection + if (includesDoc) { + //Case 1: Pres collection should not change as it is already the same + } else if (tab !== undefined) { + // Case 2: Pres collection should update + this.layoutDoc.presCollection = srcContext; } - // if (collectionDocView) { - // if (srcContext && srcContext !== presCollection) { - // // Case 1: new srcContext inside of current collection so add a new tab to the current pres collection - // collectionDocView.props.addDocTab(srcContext, "inPlace"); - // } - // } - this.updateCurrentPresentation(); - const docToJump = curDoc; - const willZoom = false; - + const presStatus = this.rootDoc.presStatus; + const selViewCache = Array.from(this._selectedArray.keys()); + const dragViewCache = Array.from(this._dragArray); + const eleViewCache = Array.from(this._eleArray); + const self = this; + const resetSelection = action(() => { + const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc); + if (presDocView) SelectionManager.SelectDoc(presDocView, false); + self.rootDoc.presStatus = presStatus; + self._selectedArray.clear(); + selViewCache.forEach(doc => self._selectedArray.set(doc, undefined)); + self._dragArray.splice(0, self._dragArray.length, ...dragViewCache); + self._eleArray.splice(0, self._eleArray.length, ...eleViewCache); + }); + const openInTab = () => { + collectionDocView ? collectionDocView.props.addDocTab(targetDoc, "") : this.props.addDocTab(targetDoc, ":left"); + this.layoutDoc.presCollection = targetDoc; + // this still needs some fixing + setTimeout(resetSelection, 500); + }; // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { - collectionDocView ? collectionDocView.props.addDocTab(activeItem, "replace") : this.props.addDocTab(activeItem, "replace:left"); - } else - //docToJump stayed same meaning, it was not in the group or was the last element in the group - if (activeItem.zoomProgressivize && this.rootDoc.presStatus !== 'edit') { - this.zoomProgressivizeNext(targetDoc); - } else if (docToJump === curDoc) { - //checking if curDoc has navigation open - if (curDoc.presMovement === PresMovement.Pan && targetDoc) { - await DocumentManager.Instance.jumpToDocument(targetDoc, false, undefined, srcContext); - } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(targetDoc, true, undefined, srcContext); - } - } else { - //awaiting jump so that new scale can be found, since jumping is async - targetDoc && await DocumentManager.Instance.jumpToDocument(targetDoc, willZoom, undefined, srcContext); - } + openInTab(); + } else if (curDoc.presMovement === PresMovement.Pan && targetDoc) { + await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right + } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right + } // After navigating to the document, if it is added as a presPinView then it will // adjust the pan and scale to that of the pinView when it was added. - // TODO: Add option to remove presPinView if (activeItem.presPinView) { // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target - const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; - bestTarget && runInAction(() => { - if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { - bestTarget._scrollY = activeItem.presPinViewScroll; - } else { - bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; - bestTarget._panX = activeItem.presPinViewX; - bestTarget._panY = activeItem.presPinViewY; - bestTarget._viewScale = activeItem.presPinViewScale; - } - }); - //setTimeout(() => targetDoc._viewTransition = undefined, 1010); + this.navigateToView(targetDoc, activeItem); } - // If website and has presWebsite data associated then on click it should - // go back to that specific website // TODO: Add progressivize for navigating web (storing websites for given frames) - if (targetDoc.presWebsiteData) { - targetDoc.data = targetDoc.presWebsiteData; - } } /** @@ -363,7 +414,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } } - /** * For 'Hide Before' and 'Hide After' buttons making sure that * they are hidden each time the presentation is updated. @@ -372,20 +422,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> onHideDocument = () => { this.childDocs.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); - const tagDoc = Cast(curDoc.presentationTargetDoc!, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); if (tagDoc) tagDoc.opacity = 1; - if (curDoc.presHideTillShownButton) { - if (index > this.itemIndex) { - tagDoc.opacity = 0; - } else if (!curDoc.presHideAfterButton) { - tagDoc.opacity = 1; + const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); + const curInd: number = itemIndexes.indexOf(index); + if (tagDoc === this.layoutDoc.presCollection) { tagDoc.opacity = 1; } + else { + if (itemIndexes.length > 1 && curDoc.presHideBefore && curInd !== 0) { } + else if (curDoc.presHideBefore) { + if (index > this.itemIndex) { + tagDoc.opacity = 0; + } else if (!curDoc.presHideAfter) { + tagDoc.opacity = 1; + } } - } - if (curDoc.presHideAfterButton) { - if (index < this.itemIndex) { - tagDoc.opacity = 0; - } else if (!curDoc.presHideTillShownButton) { - tagDoc.opacity = 1; + if (itemIndexes.length > 1 && curDoc.presHideAfter && curInd !== (itemIndexes.length - 1)) { } + else if (curDoc.presHideAfter) { + if (index < this.itemIndex) { + tagDoc.opacity = 0; + } else if (!curDoc.presHideBefore) { + tagDoc.opacity = 1; + } } } }); @@ -394,7 +451,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc - @undoBatch @action startAutoPres = (startSlide: number) => { this.updateCurrentPresentation(); @@ -418,23 +474,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (i === this.childDocs.length - 1) { setTimeout(() => { clearTimeout(this._presTimer); - if (this.layoutDoc.presStatus === 'auto' && !this.layoutDoc.presLoop) this.layoutDoc.presStatus = "manual"; + if (this.layoutDoc.presStatus === 'auto' && !this.layoutDoc.presLoop) this.layoutDoc.presStatus = PresStatus.Manual; else if (this.layoutDoc.presLoop) this.startAutoPres(0); }, duration); } } }; - this.layoutDoc.presStatus = "auto"; + this.layoutDoc.presStatus = PresStatus.Autoplay; this.startPresentation(startSlide); - this.gotoDocument(startSlide, this.itemIndex); + this.gotoDocument(startSlide); load(); } @action pauseAutoPres = () => { - if (this.layoutDoc.presStatus === "auto") { + if (this.layoutDoc.presStatus === PresStatus.Autoplay) { if (this._presTimer) clearTimeout(this._presTimer); - this.layoutDoc.presStatus = "manual"; + this.layoutDoc.presStatus = PresStatus.Manual; this.layoutDoc.presLoop = false; } } @@ -442,26 +498,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //The function that resets the presentation by removing every action done by it. It also //stops the presentaton. resetPresentation = () => { - this.updateCurrentPresentation(); this.rootDoc._itemIndex = 0; + this.childDocs.map(doc => Cast(doc.presentationTargetDoc, Doc, null)).filter(doc => doc instanceof Doc).forEach(doc => { + try { + doc.opacity = 1; + } catch (e) { + console.log("REset presentation error: ", e); + } + }); + ///for (const doc of this.childDocs) Cast(doc.presentationTargetDoc, Doc, null).opacity = 1; } @action togglePath = (srcContext: Doc, off?: boolean) => { if (off) { - this.pathBoolean = false; + this._pathBoolean = false; srcContext.presPathView = false; } else { - this.pathBoolean = !this.pathBoolean; - srcContext.presPathView = this.pathBoolean; + runInAction(() => this._pathBoolean = !this._pathBoolean); + srcContext.presPathView = this._pathBoolean; } } - @undoBatch @action toggleExpandMode = () => { - this.rootDoc.expandBoolean = !this.rootDoc.expandBoolean; + runInAction(() => this._expandBoolean = !this._expandBoolean); + this.rootDoc.expandBoolean = this._expandBoolean; this.childDocs.forEach((doc) => { - if (this.rootDoc.expandBoolean) doc.presExpandInlineButton = true; - else if (!this.rootDoc.expandBoolean) doc.presExpandInlineButton = false; + doc.presExpandInlineButton = this._expandBoolean; }); } @@ -474,10 +536,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.updateCurrentPresentation(); this.childDocs.map(doc => { const tagDoc = doc.presentationTargetDoc as Doc; - if (doc.presHideTillShownButton && this.childDocs.indexOf(doc) > startIndex) { + if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) { tagDoc.opacity = 0; } - if (doc.presHideAfterButton && this.childDocs.indexOf(doc) < startIndex) { + if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) { tagDoc.opacity = 0; } }); @@ -486,17 +548,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> /** * The method called to open the presentation as a minimized view */ - @undoBatch @action updateMinimize = () => { const docView = DocumentManager.Instance.getDocumentView(this.layoutDoc); - if (this.layoutDoc.inOverlay) { - this.layoutDoc.presStatus = 'edit'; + if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + this.layoutDoc.presStatus = PresStatus.Edit; Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); CollectionDockingView.AddSplit(this.rootDoc, "right"); - this.layoutDoc.inOverlay = false; } else if (this.layoutDoc.context && docView) { - this.layoutDoc.presStatus = 'edit'; + this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); @@ -506,7 +566,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> docView.props.removeDocument?.(this.layoutDoc); Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); } else { - this.layoutDoc.presStatus = 'edit'; + this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); @@ -522,7 +582,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * Called when the user changes the view type * Either 'List' (stacking) or 'Slides' (carousel) */ - @undoBatch + // @undoBatch viewChanged = action((e: React.ChangeEvent) => { //@ts-ignore const viewType = e.target.selectedOptions[0].value as CollectionViewType; @@ -532,32 +592,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 0; }); - /** - * When the movement dropdown is changes - */ - @undoBatch - updateMovement = action((movement: any, activeItem: Doc, targetDoc: Doc) => { - switch (movement) { - case PresMovement.Zoom: //Pan and zoom - activeItem.presMovement = PresMovement.Zoom; - break; - case PresMovement.Pan: //Pan - activeItem.presMovement = PresMovement.Pan; - break; - case PresMovement.Jump: //Jump Cut - activeItem.presJump = true; - activeItem.presMovement = PresMovement.Jump; - break; - case PresMovement.None: default: - activeItem.presMovement = PresMovement.None; - break; - } - }); setMovementName = action((movement: any, activeItem: Doc): string => { let output: string = 'none'; switch (movement) { - case PresMovement.Zoom: output = 'Zoom'; break; //Pan and zoom + case PresMovement.Zoom: output = 'Pan & Zoom'; break; //Pan and zoom case PresMovement.Pan: output = 'Pan'; break; //Pan case PresMovement.Jump: output = 'Jump cut'; break; //Jump Cut case PresMovement.None: output = 'None'; break; //None @@ -571,29 +610,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> addDocumentFilter = (doc: Doc | Doc[]) => { const docs = doc instanceof Doc ? [doc] : doc; docs.forEach((doc, i) => { - if (this.childDocs.includes(doc)) { - if (docs.length === i + 1) return false; - } else if (doc.type === DocumentType.LABEL) { + if (doc.type === DocumentType.LABEL) { const audio = Cast(doc.annotationOn, Doc, null); if (audio) { - audio.aliasOf instanceof Doc; audio.presStartTime = NumCast(doc.audioStart); audio.presEndTime = NumCast(doc.audioEnd); audio.presDuration = NumCast(doc.audioEnd) - NumCast(doc.audioStart); TabDocView.PinDoc(audio, false, true); - setTimeout(() => this.removeDocument(doc), 1); + setTimeout(() => this.removeDocument(doc), 0); return false; } } else { - doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); - !this.childDocs.includes(doc) && (doc.presMovement = PresMovement.Zoom); - if (this.rootDoc.expandBoolean) doc.presExpandInlineButton = true; + if (!doc.aliasOf) { + const original = Doc.MakeAlias(doc); + TabDocView.PinDoc(original); + setTimeout(() => this.removeDocument(doc), 0); + return false; + } else { + if (!doc.presentationTargetDoc) doc.title = doc.title + " - Slide"; + doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); + doc.presMovement = PresMovement.Zoom; + if (this._expandBoolean) doc.presExpandInlineButton = true; + } } }); return true; } childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement; - removeDocument = (doc: Doc) => { Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); }; + removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 40; active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc._isBackground) && @@ -604,55 +648,67 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> */ @action sortArray = (): Doc[] => { - return this.childDocs.filter(doc => this._selectedArray.includes(doc)); + return this.childDocs.filter(doc => this._selectedArray.has(doc)); } /** * Method to get the list of selected items in the order in which they have been selected */ @computed get listOfSelected() { - const list = this._selectedArray.map((doc: Doc, index: any) => { + const list = Array.from(this._selectedArray.keys()).map((doc: Doc, index: any) => { const curDoc = Cast(doc, Doc, null); - const tagDoc = Cast(curDoc.presentationTargetDoc!, Doc, null); - if (tagDoc) return <div className="selectedList-items">{index + 1}. {curDoc.title}</div>; - else if (curDoc) return <div className="selectedList-items">{index + 1}. {curDoc.title}</div>; + const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); + if (curDoc && curDoc === this.activeItem) return <div key={index} className="selectedList-items"><b>{index + 1}. {curDoc.title}</b></div>; + else if (tagDoc) return <div key={index} className="selectedList-items">{index + 1}. {curDoc.title}</div>; + else if (curDoc) return <div key={index} className="selectedList-items">{index + 1}. {curDoc.title}</div>; }); return list; } @action selectPres = () => { - const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc)!; - SelectionManager.SelectDoc(presDocView, false); + const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc); + presDocView && SelectionManager.SelectDoc(presDocView, false); } //Regular click @action selectElement = (doc: Doc) => { - this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex)); - if (doc.presPinView) setTimeout(() => this.selectPres(), 0); - else this.selectPres(); + const context = Cast(doc.context, Doc, null); + this.gotoDocument(this.childDocs.indexOf(doc)); + if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0); + else this.updateCurrentPresentation(context); } //Command click @action multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { - if (!this._selectedArray.includes(doc)) { - this._selectedArray.push(doc); + if (!this._selectedArray.has(doc)) { + this._selectedArray.set(doc, undefined); this._eleArray.push(ref); this._dragArray.push(drag); + } else { + this._selectedArray.delete(doc); + this.removeFromArray(this._eleArray, doc); + this.removeFromArray(this._dragArray, doc); } this.selectPres(); } + removeFromArray = (arr: any[], val: any) => { + const index: number = arr.indexOf(val); + const ret: any[] = arr.splice(index, 1); + arr = ret; + } + //Shift click @action shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { - this._selectedArray = []; + this._selectedArray.clear(); // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); if (this.activeItem) { for (let i = Math.min(this.itemIndex, this.childDocs.indexOf(doc)); i <= Math.max(this.itemIndex, this.childDocs.indexOf(doc)); i++) { - this._selectedArray.push(this.childDocs[i]); + this._selectedArray.set(this.childDocs[i], undefined); this._eleArray.push(ref); this._dragArray.push(drag); } @@ -660,68 +716,100 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.selectPres(); } + //regular click + @action + regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean) => { + this._selectedArray.clear(); + this._selectedArray.set(doc, undefined); + this._eleArray.splice(0, this._eleArray.length, ref); + this._dragArray.splice(0, this._dragArray.length, drag); + focus && this.selectElement(doc); + this.selectPres(); + } + + modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, cmdClick: boolean, shiftClick: boolean) => { + if (cmdClick) this.multiSelect(doc, ref, drag); + else if (shiftClick) this.shiftSelect(doc, ref, drag); + else this.regularSelect(doc, ref, drag, focus); + } + + static keyEventsWrapper = (e: KeyboardEvent) => { + PresBox.Instance.keyEvents(e); + } + // Key for when the presentaiton is active - @undoBatch - keyEvents = action((e: KeyboardEvent) => { + @action + keyEvents = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement) return; let handled = false; const anchorNode = document.activeElement as HTMLDivElement; if (anchorNode && anchorNode.className?.includes("lm_title")) return; - if (e.keyCode === 27) { // Escape key - if (this.layoutDoc.inOverlay) { this.updateMinimize(); } - else if (this.layoutDoc.presStatus === "edit") { this._selectedArray = []; this._eleArray = []; this._dragArray = []; } - else this.layoutDoc.presStatus = "edit"; - if (this._presTimer) clearTimeout(this._presTimer); - handled = true; - } if ((e.metaKey || e.altKey) && e.keyCode === 65) { // Ctrl-A to select all - if (this.layoutDoc.presStatus === "edit") { - this._selectedArray = this.childDocs; + switch (e.key) { + case "Backspace": + if (this.layoutDoc.presStatus === "edit") { + undoBatch(action(() => { + for (const doc of Array.from(this._selectedArray.keys())) { + this.removeDocument(doc); + } + this._selectedArray.clear(); + this._eleArray.length = 0; + this._dragArray.length = 0; + }))(); + handled = true; + } + break; + case "Escape": + if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { this.updateMinimize(); } + else if (this.layoutDoc.presStatus === "edit") { this._selectedArray.clear(); this._eleArray.length = this._dragArray.length = 0; } + else this.layoutDoc.presStatus = "edit"; + if (this._presTimer) clearTimeout(this._presTimer); handled = true; - } - } if (e.keyCode === 37 || e.keyCode === 38) { // left(37) / a(65) / up(38) to go back - this.back(); - if (this._presTimer) clearTimeout(this._presTimer); - handled = true; - } if (e.keyCode === 39 || e.keyCode === 40) { // right (39) / d(68) / down(40) to go to next - this.next(); - if (this._presTimer) clearTimeout(this._presTimer); - handled = true; - } if (e.keyCode === 32) { // spacebar to 'present' or autoplay - if (this.layoutDoc.presStatus !== "edit") this.startAutoPres(0); - else this.next(); - handled = true; - } if (e.keyCode === 8) { // delete selected items - if (this.layoutDoc.presStatus === "edit") { - this._selectedArray.forEach((doc, i) => this.removeDocument(doc)); - this._selectedArray = []; - this._eleArray = []; - this._dragArray = []; + break; + case "Down": case "ArrowDown": + case "Right": case "ArrowRight": + if (e.shiftKey && this.itemIndex < this.childDocs.length - 1) { // TODO: update to work properly + this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) + 1; + this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined); + } else { + this.next(); + if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } + } handled = true; - } - } if (handled) { + break; + case "Up": case "ArrowUp": + case "Left": case "ArrowLeft": + if (e.shiftKey && this.itemIndex !== 0) { // TODO: update to work properly + this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) - 1; + this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined); + } else { + this.back(); + if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } + } + handled = true; + break; + case "Spacebar": case " ": + if (this.layoutDoc.presStatus === PresStatus.Manual) this.startAutoPres(this.itemIndex); + else if (this.layoutDoc.presStatus === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer); + handled = true; + break; + case "a": + if ((e.metaKey || e.altKey) && this.layoutDoc.presStatus === "edit") { + this._selectedArray.clear(); + this.childDocs.forEach(doc => this._selectedArray.set(doc, undefined)); + handled = true; + } + default: + break; + } + if (handled) { e.stopPropagation(); e.preventDefault(); - } if ((e.keyCode === 37 || e.keyCode === 38) && e.shiftKey) { // left(37) / a(65) / up(38) to go back - if (this.layoutDoc.presStatus === "edit" && this._selectedArray.length > 0) { - const index = this.childDocs.indexOf(this._selectedArray[this._selectedArray.length]); - if ((index - 1) > 0) this._selectedArray.push(this.childDocs[index - 1]); - } - handled = true; - } if ((e.keyCode === 39 || e.keyCode === 40) && e.shiftKey) { // left(37) / a(65) / up(38) to go back - if (this.layoutDoc.presStatus === "edit" && this._selectedArray.length > 0) { - const index = this.childDocs.indexOf(this._selectedArray[this._selectedArray.length]); - if ((index - 1) > 0) { - this._selectedArray.push(this.childDocs[index - 1]); - } - } - handled = true; } - }); + } /** * */ - @undoBatch @action viewPaths = () => { const srcContext = Cast(this.rootDoc.presCollection, Doc, null); @@ -730,28 +818,73 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } } + getAllIndexes = (arr: Doc[], val: Doc): number[] => { + const indexes = []; + for (let i = 0; i < arr.length; i++) { + arr[i] === val && indexes.push(i); + } + return indexes; + } + // Adds the index in the pres path graphically @computed get order() { const order: JSX.Element[] = []; - this.childDocs.forEach((doc, index) => { + const docs: Doc[] = []; + const presCollection = Cast(this.rootDoc.presCollection, Doc, null); + const dv = DocumentManager.Instance.getDocumentView(presCollection); + this.childDocs.filter(doc => Cast(doc.presentationTargetDoc, Doc, null)).forEach((doc, index) => { const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); - const srcContext = Cast(tagDoc?.context, Doc, null); + const srcContext = Cast(tagDoc.context, Doc, null); const width = NumCast(tagDoc._width) / 10; const height = Math.max(NumCast(tagDoc._height) / 10, 15); const edge = Math.max(width, height); const fontSize = edge * 0.8; - // Case A: Document is contained within the colleciton - if (this.rootDoc.presCollection === srcContext) { - order.push( - <div className="pathOrder" style={{ top: NumCast(tagDoc.y) - (edge / 2), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }}> - <div className="pathOrder-frame">{index + 1}</div> - </div>); - // Case B: Document is not inside of the collection - } else { + const gap = 2; + if (presCollection === srcContext) { + // Case A: Document is contained within the collection + if (docs.includes(tagDoc)) { + const prevOccurances: number = this.getAllIndexes(docs, tagDoc).length; + docs.push(tagDoc); + order.push( + <div className="pathOrder" + key={tagDoc.id + 'pres' + index} + style={{ top: NumCast(tagDoc.y) + (prevOccurances * (edge + gap) - (edge / 2)), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }} + onClick={() => this.selectElement(doc)}> + <div className="pathOrder-frame">{index + 1}</div> + </div>); + } else { + docs.push(tagDoc); + order.push( + <div className="pathOrder" + key={tagDoc.id + 'pres' + index} + style={{ top: NumCast(tagDoc.y) - (edge / 2), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }} + onClick={() => this.selectElement(doc)}> + <div className="pathOrder-frame">{index + 1}</div> + </div>); + } + } else if (doc.presPinView && presCollection === tagDoc && dv) { + // Case B: Document is presPinView and is presCollection + const scale: number = 1 / NumCast(doc.presPinViewScale); + const height: number = dv.props.PanelHeight() * scale; + const width: number = dv.props.PanelWidth() * scale; + const indWidth = width / 10; + const indHeight = Math.max(height / 10, 15); + const indEdge = Math.max(indWidth, indHeight); + const indFontSize = indEdge * 0.8; + const xLoc: number = NumCast(doc.presPinViewX) - (width / 2); + const yLoc: number = NumCast(doc.presPinViewY) - (height / 2); + docs.push(tagDoc); order.push( - <div className="pathOrder" style={{ top: 0, left: 0 }}> - <div className="pathOrder-frame">{index + 1}</div> - </div>); + <> + <div className="pathOrder" + key={tagDoc.id + 'pres' + index} + style={{ top: yLoc - (indEdge / 2), left: xLoc - (indEdge / 2), width: indEdge, height: indEdge, fontSize: indFontSize }} + onClick={() => this.selectElement(doc)} + > + <div className="pathOrder-frame">{index + 1}</div> + </div> + <div className="pathOrder-presPinView" style={{ top: yLoc, left: xLoc, width: width, height: height, borderWidth: indEdge / 10 }}></div> + </>); } }); return order; @@ -767,17 +900,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> */ @computed get paths() { let pathPoints = ""; + const presCollection = Cast(this.rootDoc.presCollection, Doc, null); this.childDocs.forEach((doc, index) => { const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); const srcContext = Cast(tagDoc?.context, Doc, null); - if (tagDoc && this.rootDoc.presCollection === srcContext) { + if (tagDoc && presCollection === srcContext) { const n1x = NumCast(tagDoc.x) + (NumCast(tagDoc._width) / 2); const n1y = NumCast(tagDoc.y) + (NumCast(tagDoc._height) / 2); if (index = 0) pathPoints = n1x + "," + n1y; else pathPoints = pathPoints + " " + n1x + "," + n1y; - } else { - if (index = 0) pathPoints = 0 + "," + 0; - else pathPoints = pathPoints + " " + 0 + "," + 0; + } else if (doc.presPinView && presCollection === tagDoc) { + const n1x = NumCast(doc.presPinViewX); + const n1y = NumCast(doc.presPinViewY); + if (index = 0) pathPoints = n1x + "," + n1y; + else pathPoints = pathPoints + " " + n1x + "," + n1y; } }); return (<polyline @@ -796,53 +932,134 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> />); } - /** - * The function that is called on click to turn fading document after presented option on/off. - * It also makes sure that the option swithches from hide-after to this one, since both - * can't coexist. - */ - @action - onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { - e.stopPropagation(); - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - activeItem.presFadeButton = !activeItem.presFadeButton; - if (!activeItem.presFadeButton) { - if (targetDoc) { - targetDoc.opacity = 1; - } - } else { - activeItem.presHideAfterButton = false; - if (this.rootDoc.presStatus !== "edit" && targetDoc) { - targetDoc.opacity = 0.5; - } - } - } - // Converts seconds to ms and updates presTransition - @undoBatch setTransitionTime = (number: String, change?: number) => { let timeInMS = Number(number) * 1000; if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 10000) timeInMS = 10000; - if (this.activeItem) this.activeItem.presTransition = timeInMS; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presTransition = timeInMS); } // Converts seconds to ms and updates presDuration - @undoBatch setDurationTime = (number: String, change?: number) => { let timeInMS = Number(number) * 1000; if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; - if (this.activeItem) this.activeItem.presDuration = timeInMS; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presDuration = timeInMS); } + /** + * When the movement dropdown is changes + */ + @undoBatch + updateMovement = action((movement: any, all?: boolean) => { + const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); + array.forEach((doc) => { + switch (movement) { + case PresMovement.Zoom: //Pan and zoom + doc.presMovement = PresMovement.Zoom; + break; + case PresMovement.Pan: //Pan + doc.presMovement = PresMovement.Pan; + break; + case PresMovement.Jump: //Jump Cut + doc.presJump = true; + doc.presMovement = PresMovement.Jump; + break; + case PresMovement.None: default: + doc.presMovement = PresMovement.None; + break; + } + }); + }); + + @undoBatch + @action + updateHideBefore = (activeItem: Doc) => { + activeItem.presHideBefore = !activeItem.presHideBefore; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presHideBefore = activeItem.presHideBefore); + } + + @undoBatch + @action + updateHideAfter = (activeItem: Doc) => { + activeItem.presHideAfter = !activeItem.presHideAfter; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presHideAfter = activeItem.presHideAfter); + } + + @undoBatch + @action + updateOpenDoc = (activeItem: Doc) => { + activeItem.openDocument = !activeItem.openDocument; + Array.from(this._selectedArray.keys()).forEach((doc) => { + doc.openDocument = activeItem.openDocument; + }); + } + + @undoBatch + @action + updateEffectDirection = (effect: any, all?: boolean) => { + const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); + array.forEach((doc) => { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + switch (effect) { + case PresEffect.Left: + tagDoc.presEffectDirection = PresEffect.Left; + break; + case PresEffect.Right: + tagDoc.presEffectDirection = PresEffect.Right; + break; + case PresEffect.Top: + tagDoc.presEffectDirection = PresEffect.Top; + break; + case PresEffect.Bottom: + tagDoc.presEffectDirection = PresEffect.Bottom; + break; + case PresEffect.Center: default: + tagDoc.presEffectDirection = PresEffect.Center; + break; + } + }); + } + + @undoBatch + @action + updateEffect = (effect: any, all?: boolean) => { + const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); + array.forEach((doc) => { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + switch (effect) { + case PresEffect.Bounce: + tagDoc.presEffect = PresEffect.Bounce; + break; + case PresEffect.Fade: + tagDoc.presEffect = PresEffect.Fade; + break; + case PresEffect.Flip: + tagDoc.presEffect = PresEffect.Flip; + break; + case PresEffect.Roll: + tagDoc.presEffect = PresEffect.Roll; + break; + case PresEffect.Rotate: + tagDoc.presEffect = PresEffect.Rotate; + break; + case PresEffect.None: default: + tagDoc.presEffect = PresEffect.None; + break; + } + }); + } + + _batch: UndoManager.Batch | undefined = undefined; @computed get transitionDropdown() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; + const isPresCollection: boolean = (targetDoc === this.layoutDoc.presCollection); + const isPinWithView: boolean = BoolCast(activeItem.presPinView); if (activeItem && targetDoc) { const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; @@ -853,16 +1070,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = false; this.openEffectDropdown = false; })}> <div className="ribbon-box"> Movement - <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? 'solid 2px #5B9FDD' : 'solid 1px black' }}> - {this.setMovementName(activeItem.presMovement, activeItem)} - <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? '#5B9FDD' : 'black' }} icon={"angle-down"} /> - <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? "grid" : "none" }}> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None, activeItem, targetDoc)}>None</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom, activeItem, targetDoc)}>Pan and Zoom</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan, activeItem, targetDoc)}>Pan</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump, activeItem, targetDoc)}>Jump cut</div> + {isPresCollection || (isPresCollection && isPinWithView) ? + <div className="ribbon-property" style={{ marginLeft: 0, height: 25, textAlign: 'left', paddingLeft: 5, paddingRight: 5, fontSize: 10 }}> + {this.scrollable ? "Scroll to pinned view" : !isPinWithView ? "No movement" : "Pan & Zoom to pinned view"} </div> - </div> + : + <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${PresColor.DarkBlue}` : 'solid 1px black' }}> + {this.setMovementName(activeItem.presMovement, activeItem)} + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? PresColor.DarkBlue : 'black' }} icon={"angle-down"} /> + <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? "grid" : "none" }}> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None)}>None</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom)}>Pan {"&"} Zoom</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan)}>Pan</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump)}>Jump cut</div> + </div> + </div> + } <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}> <div className="presBox-subheading">Transition Speed</div> <div className="ribbon-property"> @@ -871,15 +1094,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> onChange={action((e) => this.setTransitionTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> - <div className="ribbon-propertyUpDownItem" onClick={() => this.setTransitionTime(String(transitionSpeed), 1000)}> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), 1000))}> <FontAwesomeIcon icon={"caret-up"} /> </div> - <div className="ribbon-propertyUpDownItem" onClick={() => this.setTransitionTime(String(transitionSpeed), -1000)}> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), -1000))}> <FontAwesomeIcon icon={"caret-down"} /> </div> </div> </div> - <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`} id="toolbar-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setTransitionTime(e.target.value); }} /> + <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} + className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`} + id="toolbar-slider" + onPointerDown={() => this._batch = UndoManager.StartBatch("presTransition")} + onPointerUp={() => this._batch?.end()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + this.setTransitionTime(e.target.value); + }} /> <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> @@ -889,46 +1120,51 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Visibility {"&"} Duration <div className="ribbon-doubleButton"> - <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideTillShownButton ? "active" : ""}`} onClick={undoBatch(() => activeItem.presHideTillShownButton = !activeItem.presHideTillShownButton)}>Hide before</div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfterButton ? "active" : ""}`} onClick={undoBatch(() => activeItem.presHideAfterButton = !activeItem.presHideAfterButton)}>Hide after</div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Open document in a new tab"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? "#aedef8" : "" }} onClick={undoBatch(() => activeItem.openDocument = !activeItem.openDocument)}>Open</div></Tooltip> + {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideBefore ? "active" : ""}`} onClick={() => this.updateHideBefore(activeItem)}>Hide before</div></Tooltip>} + {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfter ? "active" : ""}`} onClick={() => this.updateHideAfter(activeItem)}>Hide after</div></Tooltip>} + <Tooltip title={<><div className="dash-tooltip">{"Open document in a new tab"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? PresColor.LightBlue : "" }} onClick={() => this.updateOpenDoc(activeItem)}>Open</div></Tooltip> </div> <div className="ribbon-doubleButton" > <div className="presBox-subheading">Slide Duration</div> <div className="ribbon-property"> <input className="presBox-input" type="number" value={duration} - // onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={action((e) => this.setDurationTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> - <div className="ribbon-propertyUpDownItem" onClick={() => this.setDurationTime(String(duration), 1000)}> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}> <FontAwesomeIcon icon={"caret-up"} /> </div> - <div className="ribbon-propertyUpDownItem" onClick={() => this.setDurationTime(String(duration), -1000)}> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), -1000))}> <FontAwesomeIcon icon={"caret-down"} /> </div> </div> </div> - <input type="range" step="0.1" min="0.1" max="20" value={duration} style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "block" }} className={"toolbar-slider"} id="duration-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setDurationTime(e.target.value); }} /> + <input type="range" step="0.1" min="0.1" max="20" value={duration} + style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "block" }} + className={"toolbar-slider"} id="duration-slider" + onPointerDown={() => { this._batch = UndoManager.StartBatch("presDuration"); }} + onPointerUp={() => { if (this._batch) this._batch.end(); }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setDurationTime(e.target.value); }} + /> <div className={"slider-headers"} style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "grid" }}> <div className="slider-text">Short</div> <div className="slider-text">Medium</div> <div className="slider-text">Long</div> </div> </div> - <div className="ribbon-box"> + {isPresCollection ? (null) : <div className="ribbon-box"> Effects - <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openEffectDropdown = !this.openEffectDropdown; })} style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? 'solid 2px #5B9FDD' : 'solid 1px black' }}> + <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openEffectDropdown = !this.openEffectDropdown; })} style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${PresColor.DarkBlue}` : 'solid 1px black' }}> {effect} - <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? '#5B9FDD' : 'black' }} icon={"angle-down"} /> + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? PresColor.DarkBlue : 'black' }} icon={"angle-down"} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? "grid" : "none" }} onPointerDown={e => e.stopPropagation()}> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'None')}>None</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Fade')}>Fade In</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Flip')}>Flip</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Rotate')}>Rotate</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Bounce')}>Bounce</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Roll')}>Roll</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.None || !targetDoc.presEffect ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.None)}>None</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Fade ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>Fade In</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Flip ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}>Flip</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Rotate ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}>Rotate</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Bounce ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}>Bounce</div> + <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Roll ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>Roll</div> </div> </div> <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? "none" : "inline-flex" }}> @@ -938,17 +1174,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> <div className="effectDirection" style={{ display: effect === 'None' ? "none" : "grid", width: 40 }}> - <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "left" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={undoBatch(() => targetDoc.presEffectDirection = 'left')}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "right" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={undoBatch(() => targetDoc.presEffectDirection = 'right')}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === "top" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={undoBatch(() => targetDoc.presEffectDirection = 'top')}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === "bottom" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={undoBatch(() => targetDoc.presEffectDirection = 'bottom')}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection ? "solid 2px black" : "solid 2px #5a9edd", borderRadius: "100%", cursor: "pointer" }} onClick={undoBatch(() => targetDoc.presEffectDirection = false)}></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Left ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Left)}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Right ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Right)}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Top ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Top)}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Bottom ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Bottom)}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection === PresEffect.Center || !targetDoc.presEffectDirection ? `solid 2px ${PresColor.LightBlue}` : "solid 2px black", borderRadius: "100%", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Center)}></div></Tooltip> </div> - </div> + </div>} <div className="ribbon-final-box"> - <div className={this._selectedArray.length === 0 ? "ribbon-final-button" : "ribbon-final-button-hidden"} onClick={() => this.applyTo(this._selectedArray)}> - Apply to selected - </div> <div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}> Apply to all </div> @@ -975,19 +1208,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> applyTo = (array: Doc[]) => { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; + this.updateMovement(activeItem.presMovement, true); + this.updateEffect(targetDoc.presEffect, true); + this.updateEffectDirection(targetDoc.presEffectDirection, true); array.forEach((doc) => { const curDoc = Cast(doc, Doc, null); const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); if (tagDoc && targetDoc) { curDoc.presTransition = activeItem.presTransition; curDoc.presDuration = activeItem.presDuration; - tagDoc.presEffect = targetDoc.presEffect; - tagDoc.presEffectDirection = targetDoc.presEffectDirection; - tagDoc.presMovement = targetDoc.presMovement; - curDoc.presMovement = activeItem.presMovement; - this.updateMovement(activeItem.presMovement, curDoc, tagDoc); - curDoc.presHideTillShownButton = activeItem.presHideTillShownButton; - curDoc.presHideAfterButton = activeItem.presHideAfterButton; + curDoc.presHideBefore = activeItem.presHideBefore; + curDoc.presHideAfter = activeItem.presHideAfter; } }); } @@ -1000,12 +1231,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div> <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="ribbon-box"> - <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.AUDIO ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.playAuto ? "#aedef8" : "" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play automatically</div> - <div className="ribbon-toggle" style={{ display: "flex", backgroundColor: activeItem.playAuto ? "" : "#aedef8" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play on next</div> + <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.AUDIO ? "inline-flex" : "none" }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.playAuto ? PresColor.LightBlue : "" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play automatically</div> + <div className="ribbon-toggle" style={{ display: "flex", backgroundColor: activeItem.playAuto ? "" : PresColor.LightBlue }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play on next</div> </div> - {targetDoc.type === DocumentType.VID ? <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presVidFullScreen ? "#aedef8" : "" }} onClick={() => activeItem.presVidFullScreen = !activeItem.presVidFullScreen}>Full screen</div> : (null)} - {targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.AUDIO ? <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + {/* {targetDoc.type === DocumentType.VID ? <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presVidFullScreen ? PresColor.LightBlue : "" }} onClick={() => activeItem.presVidFullScreen = !activeItem.presVidFullScreen}>Full screen</div> : (null)} */} + {targetDoc.type === DocumentType.AUDIO ? <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> <div className="presBox-subheading">Start time</div> <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> <input className="presBox-input" @@ -1014,7 +1245,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presStartTime = Number(e.target.value); })} /> </div> </div> : (null)} - {targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.AUDIO ? <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + {targetDoc.type === DocumentType.AUDIO ? <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> <div className="presBox-subheading">End time</div> <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> <input className="presBox-input" @@ -1023,13 +1254,45 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presEndTime = Number(val); })} /> </div> </div> : (null)} - {targetDoc.type === DocumentType.COL ? 'Presentation Pin View' : (null)} - <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.COL ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? "#aedef8" : "" }} + {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : (null)} + <div className="ribbon-doubleButton"> + <Tooltip title={<><div className="dash-tooltip">{activeItem.presPinView ? "Turn off pin with view" : "Turn on pin with view"}</div></>}><div className="ribbon-toggle" style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? PresColor.LightBlue : "" }} onClick={() => { activeItem.presPinView = !activeItem.presPinView; targetDoc.presPinView = activeItem.presPinView; if (activeItem.presPinView) { + if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) { + const scroll = targetDoc._scrollTop; + activeItem.presPinView = true; + activeItem.presPinViewScroll = scroll; + } else if (targetDoc.type === DocumentType.VID) { + activeItem.presPinTimecode = targetDoc._currentTimecode; + } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeItem.presPinView = true; + activeItem.presPinViewX = x; + activeItem.presPinViewY = y; + activeItem.presPinViewScale = scale; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const width = targetDoc._clipWidth; + activeItem.presPinClipWidth = width; + activeItem.presPinView = true; + } + } + }}>{presPinWithViewIcon}</div></Tooltip> + {activeItem.presPinView ? <Tooltip title={<><div className="dash-tooltip">{"Update the pinned view with the view of the selected document"}</div></>}><div className="ribbon-button" + onClick={() => { + if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) { + const scroll = targetDoc._scrollTop; + activeItem.presPinViewScroll = scroll; + } else if (targetDoc.type === DocumentType.VID) { + activeItem.presPinTimecode = targetDoc._currentTimecode; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const clipWidth = targetDoc._clipWidth; + activeItem.presPinClipWidth = clipWidth; + } else { const x = targetDoc._panX; const y = targetDoc._panY; const scale = targetDoc._viewScale; @@ -1037,18 +1300,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> activeItem.presPinViewY = y; activeItem.presPinViewScale = scale; } - }}>{presPinWithViewIcon}</div> - {activeItem.presPinView ? <div className="ribbon-button" - onClick={() => { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeItem.presPinViewX = x; - activeItem.presPinViewY = y; - activeItem.presPinViewScale = scale; - }}>Update</div> : (null)} + }}>Update</div></Tooltip> : (null)} </div> - <div style={{ display: activeItem.presPinView ? "block" : "none" }}> + {this.panable ? <div style={{ display: activeItem.presPinView ? "block" : "none" }}> <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> <div className="presBox-subheading">Pan X</div> <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> @@ -1076,7 +1330,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScale = Number(val); })} /> </div> </div> - </div> + </div> : (null)} + {this.scrollable ? <div style={{ display: activeItem.presPinView ? "block" : "none" }}> + <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + <div className="presBox-subheading">Scroll</div> + <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> + <input className="presBox-input" + style={{ textAlign: 'left', width: 50 }} + type="number" value={NumCast(activeItem.presPinViewScroll)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScroll = Number(val); })} /> + </div> + </div> + </div> : (null)} {/* <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.WEB ? "inline-flex" : "none" }}> <div className="ribbon-toggle" onClick={this.progressivizeText}>Store original website</div> </div> */} @@ -1092,15 +1357,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div> <div className={'presBox-toolbar-dropdown'} style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="layout-container" style={{ height: 'max-content' }}> - <div className="layout" style={{ border: this.layout === 'blank' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'blank'; this.createNewSlide(this.layout); })} /> - <div className="layout" style={{ border: this.layout === 'title' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'title'; this.createNewSlide(this.layout); })}> + <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'blank'; this.createNewSlide(this.layout); })} /> + <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'title'; this.createNewSlide(this.layout); })}> <div className="title">Title</div> <div className="subtitle">Subtitle</div> </div> - <div className="layout" style={{ border: this.layout === 'header' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'header'; this.createNewSlide(this.layout); })}> + <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'header'; this.createNewSlide(this.layout); })}> <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> </div> - <div className="layout" style={{ border: this.layout === 'content' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => { this.layout = 'content'; this.createNewSlide(this.layout); })}> + <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'content'; this.createNewSlide(this.layout); })}> <div className="title" style={{ alignSelf: 'center' }}>Title</div> <div className="content">Text goes here</div> </div> @@ -1132,26 +1397,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Choose type: <div className="ribbon-doubleButton"> - <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "" : "#aedef8" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> - <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "#aedef8" : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> + <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "" : PresColor.LightBlue }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> + <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? PresColor.LightBlue : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> </div> </div> <div className="ribbon-box" style={{ display: this.addFreeform ? "grid" : "none" }}> Preset layouts: <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> - <div className="layout" style={{ border: this.layout === 'blank' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'blank')} /> - <div className="layout" style={{ border: this.layout === 'title' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'title')}> + <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'blank')} /> + <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'title')}> <div className="title">Title</div> <div className="subtitle">Subtitle</div> </div> - <div className="layout" style={{ border: this.layout === 'header' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'header')}> + <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'header')}> <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> </div> - <div className="layout" style={{ border: this.layout === 'content' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'content')}> + <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'content')}> <div className="title" style={{ alignSelf: 'center' }}>Title</div> <div className="content">Text goes here</div> </div> - <div className="layout" style={{ border: this.layout === 'twoColumns' ? 'solid 2px #5b9ddd' : '' }} onClick={action(() => this.layout = 'twoColumns')}> + <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'twoColumns')}> <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>Title</div> <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>Column one text</div> <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>Column two text</div> @@ -1183,7 +1448,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (data && presData) { data.push(doc); TabDocView.PinDoc(doc, false); - this.gotoDocument(this.childDocs.length, this.itemIndex); + this.gotoDocument(this.childDocs.length); } else { this.props.addDocTab(doc, "add:right"); } @@ -1233,10 +1498,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get presentDropdown() { return ( <div className={`dropdown-play ${this.presentTools ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> - <div className="dropdown-play-button" onClick={(action(() => { this.updateMinimize(); this.turnOffEdit(true); }))}> + <div className="dropdown-play-button" onClick={undoBatch(action(() => { this.updateMinimize(); this.turnOffEdit(true); }))}> Minimize </div> - <div className="dropdown-play-button" onClick={(action(() => { this.layoutDoc.presStatus = "manual"; this.turnOffEdit(true); }))}> + <div className="dropdown-play-button" onClick={undoBatch(action(() => { this.layoutDoc.presStatus = "manual"; this.turnOffEdit(true); }))}> Sidebar view </div> </div> @@ -1244,7 +1509,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } // Case in which the document has keyframes to navigate to next key frame - @undoBatch @action nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => { const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); @@ -1260,7 +1524,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame)); } - @undoBatch @action prevKeyframe = (tagDoc: Doc, actItem: Doc): void => { const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); @@ -1312,8 +1575,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> {/* <div className="ribbon-box"> {this.stringType} selected <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeChild}>Contents</div> - <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? "#aedef8" : "" }} onClick={this.editProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? PresColor.LightBlue : "" }} onClick={this.progressivizeChild}>Contents</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? PresColor.LightBlue : "" }} onClick={this.editProgressivize}>Edit</div> </div> <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}> <div className="presBox-subheading">Active text color</div> @@ -1328,12 +1591,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> {this.viewedColorPicker} <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeZoom}>Zoom</div> - <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? "#aedef8" : "" }} onClick={this.editZoomProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? PresColor.LightBlue : "" }} onClick={this.progressivizeZoom}>Zoom</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? PresColor.LightBlue : "" }} onClick={this.editZoomProgressivize}>Edit</div> </div> <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: targetDoc._viewType === "stacking" || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeScroll}>Scroll</div> - <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? "#aedef8" : "" }} onClick={this.editScrollProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? PresColor.LightBlue : "" }} onClick={this.progressivizeScroll}>Scroll</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? PresColor.LightBlue : "" }} onClick={this.editScrollProgressivize}>Edit</div> </div> </div> */} <div className="ribbon-final-box"> @@ -1343,7 +1606,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div key="back" title="back frame" className="backKeyframe" onClick={e => { e.stopPropagation(); this.prevKeyframe(targetDoc, activeItem); }}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> </div> - <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.editing ? "white" : "black", backgroundColor: targetDoc.editing ? "#5B9FDD" : "#AEDDF8" }} + <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.editing ? "white" : "black", backgroundColor: targetDoc.editing ? PresColor.DarkBlue : PresColor.LightBlue }} onClick={action(() => targetDoc.editing = !targetDoc.editing)} > {NumCast(targetDoc._currentFrame)} </div> @@ -1357,7 +1620,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> {this.frameListHeader} {this.frameList} </div> - <div className="ribbon-toggle" style={{ height: 20, backgroundColor: "#AEDDF8" }} onClick={() => console.log(" TODO: play frames")}>Play</div> + <div className="ribbon-toggle" style={{ height: 20, backgroundColor: PresColor.LightBlue }} onClick={() => console.log(" TODO: play frames")}>Play</div> </div> </div> </div> @@ -1406,12 +1669,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action turnOffEdit = (paths?: boolean) => { + // Turn off paths if (paths) { - // Turn off paths const srcContext = Cast(this.rootDoc.presCollection, Doc, null); if (srcContext) this.togglePath(srcContext, true); } - // Turn off the progressivize editors for each + // Turn off the progressivize editors for each document this.childDocs.forEach((doc) => { doc.editSnapZoomProgressivize = false; doc.editZoomProgressivize = false; @@ -1573,7 +1836,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? "block" : "none" }}>{this.checkMovementLists(doc, doc["x-indexed"], doc["y-indexed"])}</div>); } tags.push( - <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? "#aedff8" : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> + <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? PresColor.LightBlue : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> <div className="progressivizeButton-prev"><FontAwesomeIcon icon={"caret-left"} size={"lg"} onClick={e => { e.stopPropagation(); this.prevAppearFrame(doc, index); }} /></div> <div className="progressivizeButton-frame">{doc.appearFrame}</div> <div className="progressivizeButton-next"><FontAwesomeIcon icon={"caret-right"} size={"lg"} onClick={e => { e.stopPropagation(); this.nextAppearFrame(doc, index); }} /></div> @@ -1582,7 +1845,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> return tags; } - @undoBatch @action nextAppearFrame = (doc: Doc, i: number): void => { // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); @@ -1595,7 +1857,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.updateOpacityList(doc["opacity-indexed"], NumCast(doc.appearFrame)); } - @undoBatch @action prevAppearFrame = (doc: Doc, i: number): void => { // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); @@ -1656,30 +1917,42 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Presentation Panel" : "Open Presentation Panel"; const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const isMini: boolean = this.toolbarWidth <= 100; + const presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.UserDoc().activePresentation); return (mode === CollectionViewType.Carousel3D) ? (null) : ( - <div id="toolbarContainer" className={'presBox-toolbar'} style={{ display: this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }}> + <div id="toolbarContainer" className={'presBox-toolbar'}> {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> <FontAwesomeIcon icon={"plus"} /> <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> </div></Tooltip> */} <Tooltip title={<><div className="dash-tooltip">{"View paths"}</div></>}> - <div style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3 }} className={`toolbar-button ${this.pathBoolean ? "active" : ""}`} onClick={this.childDocs.length > 1 ? this.viewPaths : undefined}> + <div style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? PresColor.DarkBlue : 'white', width: isMini ? "100%" : undefined }} className={"toolbar-button"} onClick={this.childDocs.length > 1 ? this.viewPaths : undefined}> <FontAwesomeIcon icon={"exchange-alt"} /> </div> </Tooltip> - <div className="toolbar-divider" /> - <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.expandBoolean ? "Minimize all" : "Expand all"}</div></>}> - <div className={`toolbar-button ${this.rootDoc.expandBoolean ? "active" : ""}`} onClick={this.toggleExpandMode}> - {/* <FontAwesomeIcon icon={this.rootDoc.expandBoolean ? "eye-slash" : "eye"} /> */} - <FontAwesomeIcon icon={"eye"} /> - </div> - </Tooltip> - <div className="toolbar-divider" /> - <Tooltip title={<><div className="dash-tooltip">{propTitle}</div></>}> - <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> - <FontAwesomeIcon className={"toolbar-thumbtack"} icon={propIcon} style={{ color: CurrentUserUtils.propertiesWidth > 0 ? '#AEDDF8' : 'white' }} /> - </div> - </Tooltip> + {isMini ? (null) : + <> + <div className="toolbar-divider" /> + {/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}> + <div className={"toolbar-button"} + style={{ color: this._expandBoolean ? PresColors.DarkBlue : 'white' }} + onClick={this.toggleExpandMode}> + <FontAwesomeIcon icon={"eye"} /> + </div> + </Tooltip> + <div className="toolbar-divider" /> */} + <Tooltip title={<><div className="dash-tooltip">{presKeyEvents ? "Keys are active" : "Keys are not active - click anywhere on the presentation trail to activate keys"}</div></>}> + <div className="toolbar-button" style={{ cursor: presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> + <FontAwesomeIcon className={"toolbar-thumbtack"} icon={"keyboard"} style={{ color: presKeyEvents ? PresColor.DarkBlue : 'white' }} /> + </div> + </Tooltip> + <Tooltip title={<><div className="dash-tooltip">{propTitle}</div></>}> + <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> + <FontAwesomeIcon className={"toolbar-thumbtack"} icon={propIcon} style={{ color: CurrentUserUtils.propertiesWidth > 0 ? PresColor.DarkBlue : 'white' }} /> + </div> + </Tooltip> + </> + } </div> ); } @@ -1691,25 +1964,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> */ @computed get topPanel() { const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const isMini: boolean = this.toolbarWidth <= 100; return ( <div className="presBox-buttons" style={{ display: this.rootDoc._chromeStatus === "disabled" ? "none" : undefined }}> - <select className="presBox-viewPicker" + {isMini ? (null) : <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option> <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>3D Carousel</option> - </select> - <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length > 0 ? 1 : 0.3 }}> + </select>} + <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}> <span className={`presBox-button ${this.layoutDoc.presStatus === "edit" ? "present" : ""}`}> - <div className="presBox-button-left" onClick={() => (this.childDocs.length > 0) && (this.layoutDoc.presStatus = "manual")}> + <div className="presBox-button-left" onClick={undoBatch(() => (this.childDocs.length) && (this.layoutDoc.presStatus = "manual"))}> <FontAwesomeIcon icon={"play-circle"} /> <div style={{ display: this.props.PanelWidth() > 200 ? "inline-flex" : "none" }}> Present</div> </div> - {(mode === CollectionViewType.Carousel3D) ? (null) : <div className={`presBox-button-right ${this.presentTools ? "active" : ""}`} + {(mode === CollectionViewType.Carousel3D || isMini) ? (null) : <div className={`presBox-button-right ${this.presentTools ? "active" : ""}`} onClick={(action(() => { - if (this.childDocs.length > 0) this.presentTools = !this.presentTools; + if (this.childDocs.length) this.presentTools = !this.presentTools; }))}> <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"angle-down"} /> {this.presentDropdown} @@ -1755,7 +2029,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get frameListHeader() { return (<div className="frameList-header"> - Frames + Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : (null)} <div className={"frameList-headerButtons"}> <Tooltip title={<><div className="dash-tooltip">{"Add frame by example"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); this.newFrame(); }}> <FontAwesomeIcon icon={"plus"} onPointerDown={e => e.stopPropagation()} /> @@ -1801,21 +2075,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get playButtons() { + const presEnd: boolean = !this.layoutDoc.presLoop && (this.itemIndex === this.childDocs.length - 1); + const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0); // Case 1: There are still other frames and should go through all frames before going to next slide return (<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== "edit" ? "inline-flex" : "none" }}> - <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? '#AEDDF8' : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? PresColor.DarkBlue : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> <div className="presPanel-divider"></div> - <div className="presPanel-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> - <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === "auto" ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> - <div className="presPanel-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? "pause" : "play"} /></div></Tooltip> + <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onClick={() => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-right"} /></div> <div className="presPanel-divider"></div> - <div className="presPanel-button-text" style={{ display: this.props.PanelWidth() > 250 ? "inline-flex" : "none" }}> + <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0)}><b>1</b></div></Tooltip> + <div + className="presPanel-button-text" + onClick={() => this.gotoDocument(0)} + style={{ display: this.props.PanelWidth() > 250 ? "inline-flex" : "none" }}> Slide {this.itemIndex + 1} / {this.childDocs.length} {this.playButtonFrames} </div> <div className="presPanel-divider"></div> - {this.props.PanelWidth() > 250 ? <div className="presPanel-button-text" onClick={() => { this.layoutDoc.presStatus = "edit"; clearTimeout(this._presTimer); }}>EXIT</div> - : <div className="presPanel-button" onClick={() => this.layoutDoc.presStatus = "edit"}> + {this.props.PanelWidth() > 250 ? <div className="presPanel-button-text" onClick={undoBatch(action(() => { this.layoutDoc.presStatus = "edit"; clearTimeout(this._presTimer); }))}>EXIT</div> + : <div className="presPanel-button" onClick={undoBatch(action(() => this.layoutDoc.presStatus = "edit"))}> <FontAwesomeIcon icon={"times"} /> </div>} </div>); @@ -1823,7 +2103,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action startOrPause = () => { - if (this.layoutDoc.presStatus === "manual" || this.layoutDoc.presStatus === "edit") this.startAutoPres(this.itemIndex); + if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex); else this.pauseAutoPres(); } @@ -1833,25 +2113,29 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; - return this.layoutDoc.inOverlay ? + const presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.UserDoc().activePresentation); + const presEnd: boolean = !this.layoutDoc.presLoop && (this.itemIndex === this.childDocs.length - 1); + const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0); + return CurrentUserUtils.OverlayDocs.includes(this.rootDoc) ? <div className="miniPres"> - <div className="presPanelOverlay" style={{ display: "inline-flex", height: 35, background: '#323232', top: 0, zIndex: 3000000 }}> - <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? '#AEDDF8' : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="presPanelOverlay" style={{ display: "inline-flex", height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + PresColor.DarkBlue : undefined }}> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? PresColor.DarkBlue : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> <div className="presPanel-divider"></div> - <div className="presPanel-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> - <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === "auto" ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> - <div className="presPanel-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> + <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onClick={() => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-right"} /></div> <div className="presPanel-divider"></div> + <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0)}><b>1</b></div></Tooltip> <div className="presPanel-button-text"> Slide {this.itemIndex + 1} / {this.childDocs.length} {this.playButtonFrames} </div> <div className="presPanel-divider"></div> - <div className="presPanel-button-text" onClick={() => { this.updateMinimize(); this.layoutDoc.presStatus = "edit"; clearTimeout(this._presTimer); }}>EXIT</div> + <div className="presPanel-button-text" onClick={undoBatch(action(() => { this.updateMinimize(); this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); }))}>EXIT</div> </div> </div> : - <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined }} > + <div className="presBox-cont" style={{ minWidth: CurrentUserUtils.OverlayDocs.includes(this.layoutDoc) ? 240 : undefined }} > {this.topPanel} {this.toolbar} {this.newDocumentToolbarDropdown} @@ -1878,7 +2162,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } Scripting.addGlobal(function lookupPresBoxField(container: Doc, field: string, data: Doc) { if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data); - if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 30 : 26; + if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 35 : 31; if (field === 'presStatus') return container.presStatus; if (field === '_itemIndex') return container._itemIndex; if (field === 'presBox') return container; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 16ce749bc..f467fef12 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Doc } from "../../../fields/Doc"; +import { Doc, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; import { listSpec, makeInterface } from "../../../fields/Schema"; @@ -34,12 +34,12 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh videoLoad = () => { const aspect = this.player!.videoWidth / this.player!.videoHeight; - const nativeWidth = (this.layoutDoc._nativeWidth || 0); - const nativeHeight = (this.layoutDoc._nativeHeight || 0); + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); if (!nativeWidth || !nativeHeight) { - if (!this.layoutDoc._nativeWidth) this.layoutDoc._nativeWidth = 400; - this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / aspect; - this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; + if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); + Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); + this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; } } @@ -48,7 +48,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh const height = NumCast(this.layoutDoc._height); const canvas = document.createElement('canvas'); canvas.width = 640; - canvas.height = 640 * NumCast(this.layoutDoc._nativeHeight) / NumCast(this.layoutDoc._nativeWidth, 1); + canvas.height = 640 / (Doc.NativeAspect(this.layoutDoc) || 1); const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions if (ctx) { ctx.rect(0, 0, canvas.width, canvas.height); diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 0c0854ac2..05714f665 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,5 +1,8 @@ .videoBox { transform-origin: top left; + width: 100%; + height: 100%; + position: relative; .videoBox-viewer { opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 998ddde9a..bc69a3954 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -6,7 +6,7 @@ import * as rp from 'request-promise'; import { Doc } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; import { createSchema, makeInterface } from "../../../fields/Schema"; -import { Cast, StrCast } from "../../../fields/Types"; +import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; import { Utils, emptyFunction, returnOne, returnZero, OmitKeys } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; @@ -20,6 +20,7 @@ import "./VideoBox.scss"; import { documentSchema } from "../../../fields/documentSchemas"; import { Networking } from "../../Network"; import { SnappingManager } from "../../util/SnappingManager"; +import { SelectionManager } from "../../util/SelectionManager"; const path = require('path'); export const timeSchema = createSchema({ @@ -51,30 +52,44 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD videoLoad = () => { const aspect = this.player!.videoWidth / this.player!.videoHeight; - this.layoutDoc._nativeWidth = this.player!.videoWidth; - this.layoutDoc._nativeHeight = (this.layoutDoc._nativeWidth || 0) / aspect; + Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); + Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); this.layoutDoc._height = (this.layoutDoc._width || 0) / aspect; - this.dataDoc[this.fieldKey + "-" + "duration"] = this.player!.duration; + this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration; } @action public Play = (update: boolean = true) => { this._playing = true; - update && this.player?.play(); - update && this._youtubePlayer?.playVideo(); - this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); + try { + update && this.player?.play(); + update && this._youtubePlayer?.playVideo(); + this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); + } catch (e) { + console.log("Video Play Exception:", e); + } this.updateTimecode(); } @action public Seek(time: number) { - this._youtubePlayer?.seekTo(Math.round(time), true); + try { + this._youtubePlayer?.seekTo(Math.round(time), true); + } catch (e) { + console.log("Video Seek Exception:", e); + } this.player && (this.player.currentTime = time); } @action public Pause = (update: boolean = true) => { this._playing = false; - update && this.player?.pause(); - update && this._youtubePlayer?.pauseVideo(); - this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); + try { + update && this.player?.pause(); + update && this._youtubePlayer?.pauseVideo(); + this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); + this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true); + } catch (e) { + console.log("Video Pause Exception:", e); + } + this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused. this._playTimer = undefined; this.updateTimecode(); } @@ -82,7 +97,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @action public FullScreen() { this._fullScreen = true; this.player && this.player.requestFullscreen(); - this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); + try { + this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); + } catch (e) { + console.log("Video FullScreen Exception:", e); + } } choosePath(url: string) { @@ -97,7 +116,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const height = (this.layoutDoc._height || 0); const canvas = document.createElement('canvas'); canvas.width = 640; - canvas.height = 640 * (this.layoutDoc._nativeHeight || 0) / (this.layoutDoc._nativeWidth || 1); + canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1); const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions if (ctx) { // ctx.rect(0, 0, canvas.width, canvas.height); @@ -129,25 +148,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"))); - VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => { - if (returnedFilename) { - this.createRealSummaryLink(returnedFilename); - } - }); + VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => + returnedFilename && this.createRealSummaryLink(returnedFilename)); } } private createRealSummaryLink = (relative: string) => { const url = this.choosePath(Utils.prepend(relative)); - const width = (this.layoutDoc._width || 0); - const height = (this.layoutDoc._height || 0); + const width = this.layoutDoc._width || 0; + const height = this.layoutDoc._height || 0; const imageSummary = Docs.Create.ImageDocument(url, { - _nativeWidth: this.layoutDoc._nativeWidth, _nativeHeight: this.layoutDoc._nativeHeight, + _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-" }); - Doc.GetProto(imageSummary)["data-nativeWidth"] = this.layoutDoc._nativeWidth; - Doc.GetProto(imageSummary)["data-nativeHeight"] = this.layoutDoc._nativeHeight; + Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc)); + Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc)); imageSummary.isLinkButton = true; this.props.addDocument?.(imageSummary); DocUtils.MakeLink({ doc: imageSummary }, { doc: this.rootDoc }, "video snapshot"); @@ -156,7 +172,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @action updateTimecode = () => { this.player && (this.layoutDoc._currentTimecode = this.player.currentTime); - this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime()); + try { + this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.()); + } catch (e) { + console.log("Video Timecode Exception:", e); + } } componentDidMount() { @@ -164,11 +184,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD if (this.youtubeVideoId) { const youtubeaspect = 400 / 315; - const nativeWidth = (this.layoutDoc._nativeWidth || 0); - const nativeHeight = (this.layoutDoc._nativeHeight || 0); + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); if (!nativeWidth || !nativeHeight) { - if (!this.layoutDoc._nativeWidth) this.layoutDoc._nativeWidth = 600; - this.layoutDoc._nativeHeight = (this.layoutDoc._nativeWidth || 0) / youtubeaspect; + if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600); + Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect); this.layoutDoc._height = (this.layoutDoc._width || 0) / youtubeaspect; } } @@ -258,7 +278,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } else this._youtubeContentCreated = false; - const iframe = e.target; + this.loadYouTube(e.target); + } + private loadYouTube = (iframe: any) => { let started = true; const onYoutubePlayerStateChange = (event: any) => runInAction(() => { if (started && event.data === YT.PlayerState.PLAYING) { @@ -278,13 +300,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD () => !this.props.Document.isAnnotating && Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true }); }; - this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { - events: { - 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady, - 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, - } - }); - + if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100); + else { + (YT as any)?.ready(() => { + this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { + events: { + 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady, + 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, + } + }); + }); + } } private get uIButtons() { const curTime = (this.layoutDoc._currentTimecode || 0); @@ -346,26 +372,33 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0))); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.layoutDoc._nativeWidth || 640)} height={(this.layoutDoc._nativeHeight || 390)} + onPointerLeave={this.updateTimecode} + onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390} src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />; } @action.bound addDocumentWithTimestamp(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; - docs.forEach(doc => { - const curTime = (this.layoutDoc._currentTimecode || -1); - curTime !== -1 && (doc.displayTimecode = curTime); - }); + const curTime = NumCast(this.layoutDoc._currentTimecode); + docs.forEach(doc => doc.displayTimecode = curTime); return this.addDocument(doc); } + @computed get contentScaling() { return this.props.ContentScaling(); } contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; render() { return (<div className="videoBox" onContextMenu={this.specificContextMenu} - style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > + style={{ + transform: this.props.PanelWidth() ? undefined : `scale(${this.contentScaling})`, + width: this.props.PanelWidth() ? undefined : `${100 / this.contentScaling}%`, + height: this.props.PanelWidth() ? undefined : `${100 / this.contentScaling}%`, + pointerEvents: this.layoutDoc._isBackground ? "none" : undefined, + borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.contentScaling}px` + }} > <div className="videoBox-viewer" > <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + forceScaling={true} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} annotationsKey={this.annotationKey} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 66dc3cdcc..80e2d3ce2 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,4 +1,3 @@ -import { faMousePointer, faPen, faStickyNote } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -44,6 +43,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); static _annotationStyle: any = addStyleSheet(); + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _startX: number = 0; private _startY: number = 0; @@ -52,39 +52,52 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @observable private _marqueeWidth: number = 0; @observable private _marqueeHeight: number = 0; @observable private _marqueeing: boolean = false; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } - get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; } - set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; } @observable private _url: string = "hello"; @observable private _pressX: number = 0; @observable private _pressY: number = 0; + @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); private _selectionReactionDisposer?: IReactionDisposer; private _scrollReactionDisposer?: IReactionDisposer; + private _scrollTopReactionDisposer?: IReactionDisposer; private _moveReactionDisposer?: IReactionDisposer; - private _keyInput = React.createRef<HTMLInputElement>(); private _longPressSecondsHack?: NodeJS.Timeout; private _outerRef = React.createRef<HTMLDivElement>(); - private _iframeRef = React.createRef<HTMLIFrameElement>(); private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); private _iframeDragRef = React.createRef<HTMLDivElement>(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + get scrollHeight() { return this.webpage?.scrollHeight || 1000; } + get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; } + set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; } + get webpage() { return this._iframe?.contentDocument?.children[0]; } constructor(props: any) { super(props); - this.dataDoc[this.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 850)); - this.dataDoc[this.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, this.Document[HeightSym]() / this.Document[WidthSym]() * 850)); + if (this.dataDoc[this.fieldKey] instanceof WebField) { + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); + } } iframeLoaded = action((e: any) => { - const iframe = this._iframeRef.current; - if (iframe && iframe.contentDocument) { + const iframe = this._iframe; + if (iframe?.contentDocument) { iframe.setAttribute("enable-annotation", "true"); - iframe.contentDocument.addEventListener('pointerdown', this.iframedown, false); - iframe.contentDocument.addEventListener('scroll', this.iframeScrolled, false); - this.layoutDoc.scrollHeight = iframe.contentDocument.children?.[0].scrollHeight || 1000; - iframe.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc._scrollTop); - iframe.contentDocument.children[0].scrollLeft = NumCast(this.layoutDoc._scrollLeft); + iframe.contentDocument.addEventListener("click", undoBatch(action(e => { + let href = ""; + for (let ele = e.target; ele; ele = ele.parentElement) { + href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; + } + if (href) { + this._url = href.replace(Utils.prepend(""), Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.origin); + this.submitURL(); + } + }))); + iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); + if (this.webpage) { + this.webpage.scrollTop = NumCast(this.layoutDoc._scrollTop); + this.webpage.scrollLeft = NumCast(this.layoutDoc._scrollLeft); + } } this._scrollReactionDisposer?.(); this._scrollReactionDisposer = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }), @@ -93,17 +106,30 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); const duration = durationStr ? Number(durationStr[1]) : 1000; if (scrollY !== undefined) { - setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollY || 0)), delay); - setTimeout(() => { this.layoutDoc._scrollTop = scrollY; this.layoutDoc._scrollY = undefined; }, duration + delay); + this._forceSmoothScrollUpdate = true; + this.layoutDoc._scrollY = undefined; + setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollY || 0), () => this.layoutDoc._scrollTop = scrollY), delay); } if (scrollX !== undefined) { - setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollX || 0)), delay); - setTimeout(() => { this.layoutDoc._scrollLeft = scrollX; this.layoutDoc._scrollX = undefined; }, duration + delay); + this._forceSmoothScrollUpdate = true; + this.layoutDoc._scrollX = undefined; + setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollX || 0), () => this.layoutDoc._scrollLeft = scrollX), delay); } }, { fireImmediately: true } ); + this._scrollTopReactionDisposer = reaction(() => this.layoutDoc._scrollTop, + scrollTop => { + const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); + const duration = durationStr ? Number(durationStr[1]) : 1000; + if (scrollTop !== this._outerRef.current?.scrollTop && scrollTop !== undefined && this._forceSmoothScrollUpdate) { + this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollTop || 0), () => this._forceSmoothScrollUpdate = true); + } else this._forceSmoothScrollUpdate = true; + }, + { fireImmediately: true } + ); }); + _forceSmoothScrollUpdate = true; updateScroll = (x: Opt<number>, y: Opt<number>) => { if (y !== undefined) { @@ -117,16 +143,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; - iframedown = (e: PointerEvent) => { - this._setPreviewCursor?.(e.screenX, e.screenY, false); - } - iframeScrolled = (e: any) => { - if (e.target?.children) { - e.target.children[0].scrollLeft = 0; - const scrollTop = e.target.children[0].scrollTop; - const scrollLeft = e.target.children[0].scrollLeft; - this.layoutDoc._scrollTop = this._outerRef.current!.scrollTop = scrollTop; - this.layoutDoc._scrollLeft = this._outerRef.current!.scrollLeft = scrollLeft; + iframeWheel = (e: any) => { + if (this._forceSmoothScrollUpdate && e.target?.children) { + this.webpage && setTimeout(action(() => { + this.webpage!.scrollLeft = 0; + const scrollTop = this.webpage!.scrollTop; + const scrollLeft = this.webpage!.scrollLeft; + this._outerRef.current!.scrollTop = scrollTop; + this._outerRef.current!.scrollLeft = scrollLeft; + if (this.layoutDoc._scrollTop !== scrollTop) { + this._forceSmoothScrollUpdate = false; + this.layoutDoc._scrollTop = scrollTop; + } + if (this.layoutDoc._scrollLeft !== scrollLeft) { + this._forceSmoothScrollUpdate = false; + this.layoutDoc._scrollLeft = scrollLeft; + } + })); } } async componentDidMount() { @@ -151,13 +184,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const field = Cast(this.rootDoc[this.props.fieldKey], WebField); if (field?.url.href.indexOf("youtube") !== -1) { const youtubeaspect = 400 / 315; - const nativeWidth = NumCast(this.layoutDoc._nativeWidth); - const nativeHeight = NumCast(this.layoutDoc._nativeHeight); + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); if (field) { if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { - if (!nativeWidth) this.layoutDoc._nativeWidth = 600; - this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect; - this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; + if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600); + Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect); + this.layoutDoc._height = this.layoutDoc[WidthSym]() / youtubeaspect; } } // else it's an HTMLfield } else if (field?.url) { @@ -171,21 +204,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum componentWillUnmount() { this._moveReactionDisposer?.(); this._selectionReactionDisposer?.(); + this._scrollTopReactionDisposer?.(); this._scrollReactionDisposer?.(); document.removeEventListener("pointerup", this.onLongPressUp); document.removeEventListener("pointermove", this.onLongPressMove); - this._iframeRef.current?.contentDocument?.removeEventListener('pointerdown', this.iframedown); - this._iframeRef.current?.contentDocument?.removeEventListener('scroll', this.iframeScrolled); - } - - @action - onURLChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._url = e.target.value; + this._iframe?.removeEventListener('wheel', this.iframeWheel); } onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); } + + @undoBatch @action onUrlDrop = (e: React.DragEvent) => { const { dataTransfer } = e; @@ -297,9 +327,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum let pressedBound: ClientRect | undefined; let selectedText: string = ""; let pressedImg: boolean = false; - if (this._iframeRef.current) { - const B = this._iframeRef.current.getBoundingClientRect(); - const iframeDoc = this._iframeRef.current.contentDocument; + if (this._iframe) { + const B = this._iframe.getBoundingClientRect(); + const iframeDoc = this._iframe.contentDocument; if (B && iframeDoc) { // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top); @@ -388,17 +418,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } - - @undoBatch - @action - toggleNativeDimensions = () => { - Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth?.() || 0, this.props.NativeHeight?.() || 0); - } specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); - funcs.push({ description: (this.layoutDoc[this.fieldKey + "-contentWidth"] ? "Unfreeze" : "Freeze") + " Content Width", event: () => this.layoutDoc[this.fieldKey + "-contentWidth"] = this.layoutDoc[this.fieldKey + "-contentWidth"] ? undefined : NumCast(this.layoutDoc._nativeWidth), icon: "snowflake" }); + funcs.push({ description: (this.layoutDoc[this.fieldKey + "-contentWidth"] ? "Unfreeze" : "Freeze") + " Content Width", event: () => this.layoutDoc[this.fieldKey + "-contentWidth"] = this.layoutDoc[this.fieldKey + "-contentWidth"] ? undefined : Doc.NativeWidth(this.layoutDoc), icon: "snowflake" }); cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -416,11 +440,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href; // view = <iframe className="webBox-iframe" src={url} onLoad={e => { e.currentTarget.before((e.currentTarget.contentDocument?.body || e.currentTarget.contentDocument)?.children[0]!); e.currentTarget.remove(); }} - view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={this._iframeRef} src={url} onLoad={this.iframeLoaded} + view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded} // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; } else { - view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} />; + view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={"https://crossorigin.me/https://cs.brown.edu"} />; } return view; } @@ -468,7 +492,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } @computed get annotationLayer() { TraceMobx(); - return <div className="webBox-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight) }} ref={this._annotationLayer}> + return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => <Annotation {...this.props} showInfo={emptyFunction} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) } @@ -542,10 +566,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } else if (this._mainCont.current) { // set marquee x and y positions to the spatially transformed position + const nheight = Doc.NativeHeight(this.Document) || 1; + const nwidth = Doc.NativeWidth(this.Document) || 1; const boundingRect = this._mainCont.current.getBoundingClientRect(); - const boundingHeight = (this.Document._nativeHeight || 1) / (this.Document._nativeWidth || 1) * boundingRect.width; - this._startX = (e.clientX - boundingRect.left) / boundingRect.width * (this.Document._nativeWidth || 1); - this._startY = (e.clientY - boundingRect.top) / boundingHeight * (this.Document._nativeHeight || 1); + const boundingHeight = nheight / nwidth * boundingRect.width; + this._startX = (e.clientX - boundingRect.left) / boundingRect.width * nwidth; + this._startY = (e.clientY - boundingRect.top) / boundingHeight * nheight; this._marqueeHeight = this._marqueeWidth = 0; this._marqueeing = true; } @@ -560,9 +586,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum if (this._marqueeing && this._mainCont.current) { // transform positions and find the width and height to set the marquee to const boundingRect = this._mainCont.current.getBoundingClientRect(); - const boundingHeight = (this.Document._nativeHeight || 1) / (this.Document._nativeWidth || 1) * boundingRect.width; - const curX = (e.clientX - boundingRect.left) / boundingRect.width * (this.Document._nativeWidth || 1); - const curY = (e.clientY - boundingRect.top) / boundingHeight * (this.Document._nativeHeight || 1); + const boundingHeight = (Doc.NativeHeight(this.Document) || 1) / (Doc.NativeWidth(this.Document) || 1) * boundingRect.width; + const curX = (e.clientX - boundingRect.left) / boundingRect.width * (Doc.NativeWidth(this.Document) || 1); + const curY = (e.clientY - boundingRect.top) / boundingHeight * (Doc.NativeHeight(this.Document) || 1); this._marqueeWidth = curX - this._startX; this._marqueeHeight = curY - this._startY; this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); @@ -619,23 +645,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum marqueeX = () => this._marqueeX; marqueeY = () => this._marqueeY; marqueeing = () => this._marqueeing; - visibleHeiht = () => { + visibleHeight = () => { if (this._mainCont.current) { const boundingRect = this._mainCont.current.getBoundingClientRect(); - const scalin = (this.Document._nativeWidth || 0) / boundingRect.width; - return Math.min(boundingRect.height * scalin, this.props.PanelHeight() * scalin); + const scaling = (Doc.NativeWidth(this.Document) || 0) / boundingRect.width; + return Math.min(boundingRect.height * scaling, this.props.PanelHeight() * scaling); } return this.props.PanelHeight(); } scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop)); render() { + const scaling = Number.isFinite(this.props.ContentScaling()) ? this.props.ContentScaling() || 1 : 1; return (<div className="webBox" ref={this._mainCont} > <div className={`webBox-container`} style={{ position: undefined, - transform: `scale(${this.props.ContentScaling()})`, - width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%", - height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%", + transform: `scale(${scaling})`, + width: `${100 / scaling}% `, + height: `${100 / scaling}% `, pointerEvents: this.layoutDoc._isBackground ? "none" : undefined }} onContextMenu={this.specificContextMenu}> @@ -643,33 +670,36 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum {this.content} <div className={"webBox-outerContent"} ref={this._outerRef} style={{ - width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%", + width: `${Math.max(100, 100 / scaling)}% `, pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc._isBackground ? "all" : "none" }} - onWheel={e => e.stopPropagation()} - onPointerDown={this.onMarqueeDown} - onScroll={e => { - const iframe = this._iframeRef?.current?.contentDocument; - const outerFrame = this._outerRef.current; - if (iframe && outerFrame) { - if (iframe.children[0].scrollTop !== outerFrame.scrollTop) { - iframe.children[0].scrollTop = outerFrame.scrollTop; - } - if (iframe.children[0].scrollLeft !== outerFrame.scrollLeft) { - iframe.children[0].scrollLeft = outerFrame.scrollLeft; - } + onWheel={e => { + const target = this._outerRef.current; + if (this._forceSmoothScrollUpdate && target && this.webpage) { + setTimeout(action(() => { + target.scrollLeft = 0; + const scrollTop = target.scrollTop; + const scrollLeft = target.scrollLeft; + this.webpage!.scrollTop = scrollTop; + this.webpage!.scrollLeft = scrollLeft; + if (this.layoutDoc._scrollTop !== scrollTop) this.layoutDoc._scrollTop = scrollTop; + if (this.layoutDoc._scrollLeft !== scrollLeft) this.layoutDoc._scrollLeft = scrollLeft; + })); } - //this._outerRef.current!.scrollTop !== this._scrollTop && (this._outerRef.current!.scrollTop = this._scrollTop) - }}> + e.stopPropagation(); + }} + onPointerDown={this.onMarqueeDown} + onScroll={e => e.stopPropagation()} + > <div className={"webBox-innerContent"} style={{ - height: NumCast(this.layoutDoc.scrollHeight), + height: NumCast(this.scrollHeight, 50), pointerEvents: this.layoutDoc._isBackground ? "none" : undefined }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} annotationsKey={this.annotationKey} - VisibleHeight={this.visibleHeiht} + VisibleHeight={this.visibleHeight} focus={this.props.focus} setPreviewCursor={this.setPreviewCursor} isSelected={this.props.isSelected} diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 123946dea..7cd92b8b9 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -82,7 +82,7 @@ export class DashDocView extends React.Component<IDashDocView> { const { scale, translateX, translateY } = Utils.GetScreenTransform(outerElement); return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale); } - contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1; + contentScaling = () => Doc.NativeWidth(this._dashDoc) > 0 ? this._dashDoc![WidthSym]() / Doc.NativeWidth(this._dashDoc) : 1; outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 0d92d7062..b75cc230f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -22,8 +22,7 @@ border-style: solid; overflow-y: auto; overflow-x: hidden; - color: initial; - max-height: 100%; + color: inherit; display: flex; flex-direction: row; transition: opacity 1s; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 903bbaaa3..fe38939c5 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,7 +11,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; -import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin, UpdatingFromServer } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin, UpdatingFromServer, ForceServerWrite } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; import applyDevTools = require("prosemirror-dev-tools"); import { removeMarkWithAttrs } from "./prosemirrorPatches"; @@ -663,7 +663,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const optionItems = options && "subitems" in options ? options.subitems : []; !Doc.UserDoc().noviceMode && optionItems.push({ description: !this.Document._singleLine ? "Make Single Line" : "Make Multi Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" }); optionItems.push({ description: `${this.Document._autoHeight ? "Lock" : "Auto"} Height`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); - optionItems.push({ description: `${!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Lock" : "Unlock"} Aspect`, event: this.toggleNativeDimensions, icon: "snowflake" }); + optionItems.push({ description: `${!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? "Lock" : "Unlock"} Aspect`, event: this.toggleNativeDimensions, icon: "snowflake" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); this._downX = this._downY = Number.NaN; } @@ -805,13 +805,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp tr = tr.addMark(pos, pos + node.nodeSize, link); } }); - this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents + this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); - this.dataDoc[UpdatingFromServer] = false; + this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; } } componentDidMount() { this._cachedLinks = DocListCast(this.Document.links); + this._disposers.sidebarheight = reaction( + () => ({ annoHeight: NumCast(this.rootDoc[this.annotationKey + "-height"]), textHeight: NumCast(this.rootDoc[this.fieldKey + "-height"]) }), + ({ annoHeight, textHeight }) => { + this.layoutDoc._autoHeight && (this.rootDoc._height = Math.max(annoHeight, textHeight)); + }); this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks newLinks => { this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l)); @@ -1454,10 +1459,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public endUndoTypingBatch() { const wasUndoing = this._undoTyping; - if (this._undoTyping) { - this._undoTyping.end(); - this._undoTyping = undefined; - } + this._undoTyping?.end(); + this._undoTyping = undefined; return wasUndoing; } public static LiveTextUndo: UndoManager.Batch | undefined; @@ -1470,8 +1473,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBox.LiveTextUndo?.end(); FormattedTextBox.LiveTextUndo = undefined; - // move the richtextmenu offscreen - //if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } _lastTimedMark: Mark | undefined = undefined; @@ -1544,18 +1545,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }, 10); } else { try { - const boxHeight = Number(getComputedStyle(this._boxRef.current!).height.replace("px", "")); + const boxHeight = Number(getComputedStyle(this._boxRef.current!).height.replace("px", "")) * NumCast(this.Document._viewScale, 1); const outer = this.rootDoc[HeightSym]() - boxHeight - (this.props.ChromeHeight ? this.props.ChromeHeight() : 0); - const finalHeight = newHeight + Math.max(0, outer); - const maxsidebar = !this.sidebarWidth() ? 0 : Array.from(this._boxRef.current!.getElementsByClassName("collectionStackingViewFieldColumn")).reduce((prev, ele) => Math.max(prev, Number(getComputedStyle(ele).height.replace("px", ""))), 0); - if (this.rootDoc._height !== finalHeight && finalHeight > maxsidebar) { - this.rootDoc._height = finalHeight; - this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; - } - this.rootDoc[this.fieldKey + "-height"] = finalHeight; + this.rootDoc[this.fieldKey + "-height"] = newHeight + Math.max(0, outer); } catch (e) { console.log("Error in tryUpdateHeight"); } } - } else this.rootDoc[this.fieldKey + "-height"] = 0; + } //else this.rootDoc[this.fieldKey + "-height"] = 0; } @computed get audioHandle() { @@ -1574,10 +1569,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp />; } + sidebarContentScaling = () => this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); @computed get sidebarCollection() { const fitToBox = this.props.Document._fitToBox; const collectionProps = { ...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit, + NativeWidth: returnZero, + NativeHeight: returnZero, PanelHeight: this.props.PanelHeight, PanelWidth: this.sidebarWidth, xMargin: 0, @@ -1591,7 +1589,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp isSelected: this.props.isSelected, select: emptyFunction, active: this.annotationsActive, - ContentScaling: returnOne, + ContentScaling: this.sidebarContentScaling, whenActiveChanged: this.whenActiveChanged, removeDocument: this.removeDocument, moveDocument: this.moveDocument, @@ -1610,7 +1608,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); - sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()) / this.props.ContentScaling(), 0); + sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()) / this.props.ContentScaling(), 0).scale(1 / NumCast(this.layoutDoc._viewScale, 1)); @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); } render() { TraceMobx(); @@ -1626,12 +1624,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0); const selclass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : ""; return ( - <div className={"formattedTextBox-cont"} ref={this._boxRef} + <div className="formattedTextBox-cont" ref={this._boxRef} style={{ transform: `scale(${scale})`, transformOrigin: "top left", width: `${100 / scale}%`, height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`, + overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, ...this.styleFromLayoutString(scale) // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > }}> <div className={`formattedTextBox-cont`} ref={this._ref} diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 32038d1ee..3e5a40084 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -320,8 +320,8 @@ export class FormattedTextBoxComment { whenActiveChanged={returnFalse} bringToFront={returnFalse} ContentScaling={returnOne} - NativeWidth={target._nativeWidth ? (() => NumCast(target._nativeWidth)) : undefined} - NativeHeight={target._natvieHeight ? (() => NumCast(target._nativeHeight)) : undefined} + NativeWidth={Doc.NativeWidth(target) ? (() => Doc.NativeWidth(target)) : undefined} + NativeHeight={Doc.NativeHeight(target) ? (() => Doc.NativeHeight(target)) : undefined} /> </div> </div>; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 2700c508b..cf9b03308 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -31,6 +31,7 @@ const { toggleMark } = require("prosemirror-commands"); export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: RichTextMenu; public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable + private _linkToRef = React.createRef<HTMLInputElement>(); @observable public view?: EditorView; public editorProps: FieldViewProps & FormattedTextBoxProps | undefined; @@ -154,6 +155,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @action public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) { + if (this._linkToRef.current?.getBoundingClientRect().width) { + return; + } this.view = view; if (!view || !view.hasFocus()) { return; @@ -792,7 +796,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const self = this; function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) { - self.TextView.endUndoTypingBatch(); + self.TextView?.endUndoTypingBatch(); UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), "link change"); } @@ -807,7 +811,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const dropdownContent = <div className="dropdown link-menu"> <p>Linked to:</p> - <input value={link} placeholder="Enter URL" onChange={onLinkChange} /> + <input value={link} ref={this._linkToRef} placeholder="Enter URL" onChange={onLinkChange} /> <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "add:right")}>Apply hyperlink</button> <div className="divider"></div> <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index 1767a04de..40c1d1cac 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -31,7 +31,7 @@ export class DashDocView { } //moved - contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1; + contentScaling = () => Doc.NativeWidth(this._dashDoc) > 0 ? this._dashDoc![WidthSym]() / Doc.NativeWidth(this._dashDoc) : 1; //moved outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index a8a6e8c33..48c7b1762 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -73,7 +73,7 @@ interface IViewerProps { renderDepth: number; focus: (doc: Doc) => void; isSelected: (outsideReaction?: boolean) => boolean; - loaded: (nw: number, nh: number, np: number) => void; + loaded?: (nw: number, nh: number, np: number) => void; active: (outsideReaction?: boolean) => boolean; isChildActive: (outsideReaction?: boolean) => boolean; addDocTab: (document: Doc, where: string) => boolean; @@ -238,7 +238,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]), height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) }); - i === this.props.pdf.numPages - 1 && this.props.loaded((page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]), + i === this.props.pdf.numPages - 1 && this.props.loaded?.((page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]), (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]), i); })))); this.Document.scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72; @@ -681,14 +681,14 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu getCoverImage = () => { - if (!this.props.Document[HeightSym]() || !this.props.Document._nativeHeight) { + if (!this.props.Document[HeightSym]() || !Doc.NativeHeight(this.props.Document)) { setTimeout((() => { this.Document._height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width; - this.Document._nativeHeight = (this.Document._nativeWidth || 0) * this._coverPath.height / this._coverPath.width; + Doc.SetNativeWidth(this.Document, (Doc.NativeWidth(this.Document) || 0) * this._coverPath.height / this._coverPath.width); }).bind(this), 0); } - const nativeWidth = (this.Document._nativeWidth || 0); - const nativeHeight = (this.Document._nativeHeight || 0); + const nativeWidth = Doc.NativeWidth(this.Document); + const nativeHeight = Doc.NativeHeight(this.Document); const resolved = Utils.prepend(this._coverPath.path); return <img key={resolved} src={resolved} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)} style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />; @@ -708,7 +708,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @computed get annotationLayer() { TraceMobx(); - return <div className="pdfViewerDash-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> + return <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.Document), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => <Annotation {...this.props} showInfo={this.showInfo} select={this.props.select} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) } @@ -726,11 +726,15 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); overlayTransform = () => this.scrollXf().scale(1 / this._zoomed); - panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0); - panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0); + panelWidth = () => (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); + panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); @computed get overlayLayer() { return <div className={`pdfViewerDash-overlay${Doc.GetSelectedTool() !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} - style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined, transform: `scale(${this._zoomed})` }}> + style={{ + pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined, + mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? "hard-light" : undefined, + transform: `scale(${this._zoomed})` + }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath} annotationsKey={this.annotationKey} @@ -783,13 +787,13 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} style={{ overflowX: this._zoomed !== 1 ? "scroll" : undefined, - width: !this.props.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`, - height: !this.props.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`, + width: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeWidth(this.props.Document) : `${100 / this.contentScaling}%`, + height: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeHeight(this.props.Document) : `${100 / this.contentScaling}%`, transform: `scale(${this.props.ContentScaling()})` }} > {this.pdfViewerDiv} - {this.overlayLayer} {this.annotationLayer} + {this.overlayLayer} {this.overlayInfo} {this.standinViews} <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss index f1bdb7737..73a08b6de 100644 --- a/src/client/views/presentationview/PresElementBox.scss +++ b/src/client/views/presentationview/PresElementBox.scss @@ -2,7 +2,6 @@ $light-blue: #AEDDF8; $dark-blue: #5B9FDD; $light-background: #ececec; $slide-background: #d5dce2; -$slide-hover: #98b7da; $slide-active: #5B9FDD; .presItem-container { @@ -25,7 +24,7 @@ $slide-active: #5B9FDD; align-items: center; .presItem-number { - margin-top: 7px; + margin-top: 3.5px; font-size: 12px; font-weight: 700; text-align: center; @@ -36,7 +35,6 @@ $slide-active: #5B9FDD; overflow: hidden; } - } .presItem-slide { @@ -44,19 +42,20 @@ $slide-active: #5B9FDD; background-color: #d5dce2; border-radius: 5px; height: calc(100% - 7px); - width: calc(100% - 5px); + width: calc(100% - 15px); display: grid; - grid-template-rows: 23px auto; + grid-template-rows: 16px 10px auto; grid-template-columns: max-content max-content max-content max-content auto; .presItem-name { + min-width: 20px; z-index: 300; + top: 2px; align-self: center; - font-size: 13px; + font-size: 11px; font-family: Roboto; font-weight: 500; position: relative; - top: 1px; padding-left: 10px; padding-right: 10px; letter-spacing: normal; @@ -66,20 +65,40 @@ $slide-active: #5B9FDD; white-space: pre; } + .presItem-docName { + min-width: 20px; + z-index: 300; + align-self: center; + font-size: 9px; + font-family: Roboto; + font-weight: 300; + position: relative; + padding-left: 10px; + padding-right: 10px; + letter-spacing: normal; + width: max-content; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; + grid-row: 2; + grid-column: 1/6; + } + .presItem-time { align-self: center; position: relative; - top: 2px; padding-right: 10px; + top: 1px; font-size: 10; font-weight: 300; font-family: Roboto; z-index: 300; letter-spacing: normal; } - + .presItem-embedded { overflow: hidden; + grid-row: 3; grid-column: 1/8; position: relative; display: flex; @@ -90,7 +109,7 @@ $slide-active: #5B9FDD; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; } - + .presItem-embeddedMask { width: 100%; height: 100%; @@ -106,6 +125,7 @@ $slide-active: #5B9FDD; .presItem-slideButtons { display: flex; grid-column: 7; + grid-row: 1/3; width: 60px; justify-self: right; justify-content: flex-end; @@ -115,10 +135,10 @@ $slide-active: #5B9FDD; position: relative; border-radius: 100%; z-index: 300; - width: 15px; - height: 15px; + width: 18px; + height: 18px; display: flex; - font-size: 10px; + font-size: 12px; justify-self: center; align-self: center; background-color: rgba(0, 0, 0, 0.5); @@ -131,17 +151,52 @@ $slide-active: #5B9FDD; .slideButton:hover { background-color: rgba(0, 0, 0, 1); - transform: scale(1.15); + transform: scale(1.2); } } } + + .presItem-slide.active { box-shadow: 0 0 0px 1.5px $dark-blue; } -// .presItem-slide:hover { -// background: $slide-hover; -// } +.presItem-multiDrag { + font-family: Roboto; + font-weight: 600; + color: white; + text-align: center; + justify-content: center; + align-content: center; + width: 100px; + height: 30px; + position: absolute; + background-color: $dark-blue; + z-index: 4000; + border-radius: 10px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + line-height: 30px; +} + +.presItem-miniSlide { + font-weight: 700; + font-size: 12; + grid-column: 1/8; + align-self: center; + justify-self: center; + background-color: #d5dce2; + width: 26px; + text-align: center; + height: 26px; + line-height: 28px; + border-radius: 100%; +} +.presItem-miniSlide.active { + box-shadow: 0 0 0px 1.5px $dark-blue; +} +// .presItem-slide:hover { +// background: $slide-hover; +// }
\ No newline at end of file diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index ab4cadab0..7b4afeb69 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, reaction, runInAction, observable } from "mobx"; +import { action, computed, IReactionDisposer, reaction, runInAction, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, DataSym, DocListCast } from "../../../fields/Doc"; import { documentSchema } from '../../../fields/documentSchemas'; @@ -20,6 +20,7 @@ import { DragManager } from "../../util/DragManager"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; +import { DocumentManager } from "../../util/DocumentManager"; export const presSchema = createSchema({ presentationTargetDoc: Doc, @@ -119,14 +120,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc if (this.rootDoc.type === DocumentType.AUDIO) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; } else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000; else durationInS = 2; - return this.rootDoc.presMovement === PresMovement.Jump ? (null) : "D: " + durationInS + "s"; + return "D: " + durationInS + "s"; } @computed get transition() { let transitionInS: number; if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000; else transitionInS = 0.5; - return "M: " + transitionInS + "s"; + return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s"; } private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -140,16 +141,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.stopPropagation(); e.preventDefault(); if (element && !(e.ctrlKey || e.metaKey)) { - if (PresBox.Instance._eleArray.includes(this._itemRef.current!)) { + if (PresBox.Instance._selectedArray.has(this.rootDoc)) { + PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false); setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); } else { - PresBox.Instance._selectedArray = []; - PresBox.Instance._selectedArray.push(this.rootDoc); - PresBox.Instance._eleArray = []; - PresBox.Instance._eleArray.push(this._itemRef.current!); - PresBox.Instance._dragArray = []; - PresBox.Instance._dragArray.push(this._dragRef.current!); - setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); + setupMoveUpEvents(this, e, ((e: PointerEvent) => { + PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false); + return this.startDrag(e); + }), emptyFunction, emptyFunction); } } } @@ -159,25 +158,30 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.preventDefault(); } - @action - stopDrag = (e: PointerEvent) => { - this._dragging = false; - e.stopPropagation(); - e.preventDefault(); - } - - startDrag = (e: PointerEvent, down: number[], delta: number[]) => { + startDrag = (e: PointerEvent) => { + const miniView: boolean = this.toolbarWidth <= 100; const activeItem = this.rootDoc; - const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray().map(doc => doc)); + const dragArray = PresBox.Instance._dragArray; + const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray()); const dragItem: HTMLElement[] = []; - PresBox.Instance._dragArray.map(ele => { - const doc = ele; - doc.className = "presItem-slide"; + if (dragArray.length === 1) { + const doc = dragArray[0]; + doc.className = miniView ? "presItem-miniSlide" : "presItem-slide"; dragItem.push(doc); - }); + } else if (dragArray.length >= 1) { + const doc = document.createElement('div'); + doc.className = "presItem-multiDrag"; + doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides"; + doc.style.position = 'absolute'; + doc.style.top = (e.clientY) + 'px'; + doc.style.left = (e.clientX - 50) + 'px'; + dragItem.push(doc); + } + + // const dropEvent = () => runInAction(() => this._dragging = false); if (activeItem) { - DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY); - runInAction(() => this._dragging = true); + DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined); + // runInAction(() => this._dragging = true); return true; } return false; @@ -190,7 +194,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc onPointerMove = (e: PointerEvent) => { const slide = this._itemRef.current!; - if (slide && DragManager.docsBeingDragged.length > 0) { + let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false; + for (const doc of DragManager.docsBeingDragged) { + if (!doc.presentationTargetDoc) dragIsPresItem = false; + } + if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); const y = e.clientY - rect.top; //y position within the element. const height = slide.clientHeight; @@ -222,33 +230,36 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc @undoBatch removeItem = action((e: React.MouseEvent) => { this.props.removeDocument?.(this.rootDoc); - if (PresBox.Instance._selectedArray.includes(this.rootDoc)) { - PresBox.Instance._selectedArray.splice(PresBox.Instance._selectedArray.indexOf(this.rootDoc), 1); + if (PresBox.Instance._selectedArray.has(this.rootDoc)) { + PresBox.Instance._selectedArray.delete(this.rootDoc); } e.stopPropagation(); }); + @undoBatch @action onSetValue = (value: string) => { - this.rootDoc.title = value; + this.rootDoc.title = !value.trim().length ? "-untitled-" : value; return true; } - @action - clearArrays = () => { - PresBox.Instance._eleArray = []; - PresBox.Instance._eleArray.push(this._itemRef.current!); - PresBox.Instance._dragArray = []; - PresBox.Instance._dragArray.push(this._dragRef.current!); - } - + /** + * Method called for updating the view of the currently selected document + * + * @param targetDoc + * @param activeItem + */ @undoBatch @action - pinWithView = (targetDoc: Doc, activeItem: Doc) => { - console.log(targetDoc.type); + updateView = (targetDoc: Doc, activeItem: Doc) => { if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) { const scroll = targetDoc._scrollTop; activeItem.presPinViewScroll = scroll; + } else if (targetDoc.type === DocumentType.VID) { + activeItem.presPinTimecode = targetDoc._currentTimecode; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const clipWidth = targetDoc._clipWidth; + activeItem.presPinClipWidth = clipWidth; } else { const x = targetDoc._panX; const y = targetDoc._panY; @@ -259,11 +270,21 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc } } + @computed + get toolbarWidth(): number { + const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); + let width: number = NumCast(this.presBox._width); + if (presBoxDocView) width = presBoxDocView.props.PanelWidth(); + if (width === 0) width = 300; + return width; + } + @computed get mainItem() { - const isSelected: boolean = PresBox.Instance._selectedArray.includes(this.rootDoc); - const toolbarWidth: number = PresBox.Instance.toolbarWidth; - const showMore: boolean = PresBox.Instance.toolbarWidth >= 300; - const targetDoc: Doc = Cast(this.rootDoc.presentationTargetDoc, Doc, null); + const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc); + const toolbarWidth: number = this.toolbarWidth; + const showMore: boolean = this.toolbarWidth >= 300; + const miniView: boolean = this.toolbarWidth <= 110; + const targetDoc: Doc = this.targetDoc; const activeItem: Doc = this.rootDoc; return ( <div className={`presItem-container`} key={this.props.Document[Id] + this.indexInPres} @@ -272,53 +293,51 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc onClick={e => { e.stopPropagation(); e.preventDefault(); - // Command/ control click - if (e.ctrlKey || e.metaKey) { - PresBox.Instance.multiSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!); - // Shift click - } else if (e.shiftKey) { - PresBox.Instance.shiftSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!); - // Regular click - } else { - this.props.focus(this.rootDoc); - this.clearArrays(); - } + PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); }} - onDoubleClick={e => { + onDoubleClick={action(e => { this.toggleProperties(); - this.props.focus(this.rootDoc); - this.clearArrays(); - }} + PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); + })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} onPointerDown={this.headerDown} onPointerUp={this.headerUp} > - <div className="presItem-number"> - {`${this.indexInPres + 1}.`} - </div> - <div ref={this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}> - <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 175) : toolbarWidth - 85 }}> - {isSelected ? <EditableView + {miniView ? + <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}> + {`${this.indexInPres + 1}.`} + </div> + : + <div className="presItem-number"> + {`${this.indexInPres + 1}.`} + </div>} + {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} style={{ backgroundColor: this.props.backgroundColor?.(this.layoutDoc, this.props.renderDepth) }}> + <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 185) : toolbarWidth - 95, cursor: isSelected ? 'text' : 'grab' }}> + <EditableView ref={this._titleRef} - contents={this.rootDoc.title} - GetValue={() => StrCast(this.rootDoc.title)} - SetValue={action((value: string) => { - this.onSetValue(value); - return true; - })} - /> : - this.rootDoc.title - } + editing={!isSelected ? false : undefined} + contents={activeItem.title} + overflow={'ellipsis'} + GetValue={() => StrCast(activeItem.title)} + SetValue={this.onSetValue} + /> </div> <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> <div className={"presItem-slideButtons"}> <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}> <div className="slideButton" - onClick={() => this.pinWithView(targetDoc, activeItem)} + onClick={() => this.updateView(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div> </Tooltip> + {/* <Tooltip title={<><div className="dash-tooltip">{"Group with up"}</div></>}> + <div className="slideButton" + onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp} + style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}> + <FontAwesomeIcon icon={""} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> */} <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} /> </div></Tooltip> @@ -328,16 +347,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> </div></Tooltip> </div> + <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 185) : toolbarWidth - 95 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> {this.renderEmbeddedInline} - </div> + </div>} </div >); } render() { - let item = null; - if (!(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise) item = null; - else item = this.mainItem; - - return item; + return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem; } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index bc00e93a5..3adeb6133 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -25,6 +25,7 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; import { undoBatch } from "../../util/UndoManager"; import { DocServer } from "../../DocServer"; +import { MainView } from "../MainView"; export const searchSchema = createSchema({ Document: Doc }); @@ -53,6 +54,8 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc private newsearchstring = ""; private collectionRef = React.createRef<HTMLDivElement>(); + + @observable _undoBackground: string | undefined = ""; @observable _icons: string[] = this._allIcons; @observable _results: [Doc, string[], string[]][] = []; @observable _visibleElements: JSX.Element[] = []; @@ -354,7 +357,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc doc.x = x; doc.y = y; const size = 200; - const aspect = NumCast(doc._nativeHeight) / NumCast(doc._nativeWidth, 1); + const aspect = Doc.NativeHeight(doc) / (Doc.NativeWidth(doc) || 1); if (aspect > 1) { doc._height = size; doc._width = size / aspect; @@ -498,7 +501,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); return ( <div style={{ pointerEvents: "all" }} className="searchBox-container"> - <div className="searchBox-bar"> + <div className="searchBox-bar" style={{ background: SearchBox.Instance._undoBackground }}> <div className="searchBox-lozenges" > <div className="searchBox-lozenge-user"> {`${Doc.CurrentUserEmail}`} @@ -506,7 +509,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc Logoff </div> </div> - <div className="searchBox-lozenge" onClick={() => DocServer.PRINT_CACHE()}> + <div className="searchBox-lozenge" onClick={() => DocServer.UPDATE_SERVER_CACHE()}> {`UI project`} </div> <div className="searchBox-lozenge-dashboard" > diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 1a062fa3b..4c3c45d92 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -25,6 +25,7 @@ import JSZip = require("jszip"); import { saveAs } from "file-saver"; import { CollectionDockingView } from "../client/views/collections/CollectionDockingView"; import { SelectionManager } from "../client/util/SelectionManager"; +import { DocumentView } from "../client/views/nodes/DocumentView"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -91,6 +92,7 @@ export const AclAddonly = Symbol("AclAddonly"); export const AclEdit = Symbol("AclEdit"); export const AclAdmin = Symbol("AclAdmin"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); +export const ForceServerWrite = Symbol("ForceServerWrite"); export const CachedUpdates = Symbol("Cached updates"); const AclMap = new Map<string, symbol>([ @@ -184,9 +186,10 @@ export class Doc extends RefField { @observable public [AclSym]: { [key: string]: symbol }; private [UpdatingFromServer]: boolean = false; + private [ForceServerWrite]: boolean = false; private [Update] = (diff: any) => { - !this[UpdatingFromServer] && DocServer.UpdateField(this[Id], diff); + (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff); } private [Self] = this; @@ -679,7 +682,7 @@ export namespace Doc { templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true); - setTimeout(() => { + setTimeout(action(() => { const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]"); // the template's arguments are stored in params which is derefenced to find // the actual field key where the parameterized template data is stored. @@ -693,7 +696,7 @@ export namespace Doc { targetDoc[expandedLayoutFieldKey] = newLayoutDoc; _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args); - }); + })); } } } @@ -882,6 +885,15 @@ export namespace Doc { export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; } export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; } export function LayoutFieldKey(doc: Doc): string { return StrCast(Doc.Layout(doc).layout).split("'")[1]; } + export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) { + return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1); + } + export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeWidth"], useWidth ? doc[WidthSym]() : 0)); } + export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { return !doc ? 0 : NumCast(doc._nativeHeight, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeHeight"], useHeight ? doc[HeightSym]() : 0)); } + export function SetNativeWidth(doc: Doc, width: number | undefined) { doc[Doc.LayoutFieldKey(doc) + "-nativeWidth"] = width; } + export function SetNativeHeight(doc: Doc, height: number | undefined) { doc[Doc.LayoutFieldKey(doc) + "-nativeHeight"] = height; } + + const manager = new DocData(); export function SearchQuery(): string { return manager._searchQuery; } export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); } @@ -1086,14 +1098,14 @@ export namespace Doc { export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) { runInAction(() => { - if (layoutDoc._nativeWidth || layoutDoc._nativeHeight) { + if (Doc.NativeWidth(layoutDoc) || Doc.NativeHeight(layoutDoc)) { layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale; layoutDoc._nativeWidth = undefined; layoutDoc._nativeHeight = undefined; } else { layoutDoc._autoHeight = false; - if (!layoutDoc._nativeWidth) { + if (!Doc.NativeWidth(layoutDoc)) { layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth); layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight); } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 47efccc99..024017302 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -196,7 +196,7 @@ export class ComputedField extends ScriptField { } Scripting.addGlobal(function getIndexVal(list: any[], index: number) { - return list.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any); + return list?.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any); }, "returns the value at a given index of a list", "(list: any[], index: number)"); Scripting.addGlobal(function makeScript(script: string) { diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 71294c59c..e0404d9d3 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -15,11 +15,11 @@ export const documentSchema = createSchema({ // "Location" properties in a very general sense _curPage: "number", // current page of a page based document _currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide) + _fullScreenView: Doc, // alias to display when double-clicking to open document in a full-screen view lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide) activeFrame: "number", // the active frame of a frame based animated document _currentTimecode: "number", // current play back time of a temporal document (video / audio) displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) - inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently isLabel: "boolean", // whether the document is a label or not (video / audio) audioStart: "number", // the time frame where the audio should begin playing audioEnd: "number", // the time frame where the audio should stop playing @@ -34,6 +34,7 @@ export const documentSchema = createSchema({ _scrollLeft: "number", // scroll position of a scrollable document (pdf, text, web) // appearance properties on the layout + "_backgroundGrid-spacing": "number", // the size of the grid for collection views _autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents _nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set _nativeHeight: "number", // " diff --git a/src/fields/util.ts b/src/fields/util.ts index a374c7f54..ecb3fb343 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -379,12 +379,12 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any diff?.op === "$addToSet" ? { redo: () => { - receiver[prop].push(...diff.items.map((item: any) => item.value())); + receiver[prop].push(...diff.items.map((item: any) => item.value ? item.value() : item)); lastValue = ObjectField.MakeCopy(receiver[prop]); }, undo: action(() => { - diff.items.forEach((doc: any) => { - const ind = receiver[prop].indexOf(doc.value()); + diff.items.forEach((item: any) => { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); ind !== -1 && receiver[prop].splice(ind, 1); }); lastValue = ObjectField.MakeCopy(receiver[prop]); @@ -393,16 +393,16 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any diff?.op === "$remFromSet" ? { redo: action(() => { - diff.items.forEach((doc: any) => { - const ind = receiver[prop].indexOf(doc.value()); + diff.items.forEach((item: any) => { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); ind !== -1 && receiver[prop].splice(ind, 1); }); lastValue = ObjectField.MakeCopy(receiver[prop]); }), undo: () => { - diff.items.map((item: any) => { - const ind = (prevValue as List<any>).indexOf(diff.items[0].value()); - ind !== -1 && receiver[prop].indexOf(diff.items[0].value()) === -1 && receiver[prop].splice(ind, 0, item); + diff.items.forEach((item: any) => { + const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); }); lastValue = ObjectField.MakeCopy(receiver[prop]); } diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index fd9bc0c83..d237869ed 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -114,8 +114,8 @@ export namespace Email { const smtpTransport = nodemailer.createTransport({ service: 'Gmail', auth: { - user: 'brownptcdash@gmail.com', - pass: 'browngfx1' + user: 'browndashptc@gmail.com', + pass: 'TsarNicholas#2' } }); @@ -149,7 +149,7 @@ export namespace Email { export async function dispatch({ to, subject, content, attachments }: DispatchOptions<string>): Promise<Error | null> { const mailOptions = { to, - from: 'brownptcdash@gmail.com', + from: 'browndashptc@gmail.com', subject, text: `Hello ${to.split("@")[0]},\n\n${content}`, attachments diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index fa2f6002a..e37f8c6db 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -12,9 +12,9 @@ export default class SessionManager extends ApiManager { private authorizedAction = (handler: SecureHandler) => { return (core: AuthorizedCore) => { - const { req: { params }, res, isRelease } = core; - if (!isRelease) { - return res.send("This can be run only on the release server."); + const { req: { params }, res } = core; + if (!process.env.MONITORED) { + return res.send("This command only makes sense in the context of a monitored session."); } if (params.session_key !== process.env.session_key) { return _permission_denied(res, permissionError); diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index ab3dfffcc..03ba33fee 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -12,7 +12,7 @@ import rimraf = require("rimraf"); import { AppliedSessionAgent, ExitHandler } from "./Session/agents/applied_session_agent"; import { ServerWorker } from "./Session/agents/server_worker"; import { Monitor } from "./Session/agents/monitor"; -import { MessageHandler } from "./Session/agents/promisified_ipc_manager"; +import { MessageHandler, ErrorLike } from "./Session/agents/promisified_ipc_manager"; /** * If we're the monitor (master) thread, we should launch the monitor logic for the session. @@ -70,7 +70,7 @@ export class DashSessionAgent extends AppliedSessionAgent { * Prepares the body of the email with information regarding a crash event. */ private _crashInstructions: string | undefined; - private generateCrashInstructions({ name, message, stack }: Error): string { + private generateCrashInstructions({ name, message, stack }: ErrorLike): string { if (!this._crashInstructions) { this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" }); } @@ -109,7 +109,7 @@ export class DashSessionAgent extends AppliedSessionAgent { /** * This sends an email with the generated crash report. */ - private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => { + private dispatchCrashReport: MessageHandler<{ error: ErrorLike }> = async ({ error: crashCause }) => { const { mainLog } = this.sessionMonitor; const { notificationRecipient } = DashSessionAgent; const error = await Email.dispatch({ @@ -127,7 +127,7 @@ export class DashSessionAgent extends AppliedSessionAgent { /** * Logic for interfacing with Solr. Either starts it, - * stops it, or rebuilds its indicies. + * stops it, or rebuilds its indices. */ private executeSolrCommand = async (args: string[]): Promise<void> => { const { exec, mainLog } = this.sessionMonitor; @@ -224,6 +224,6 @@ export class DashSessionAgent extends AppliedSessionAgent { export namespace DashSessionAgent { - export const notificationRecipient = "brownptcdash@gmail.com"; + export const notificationRecipient = "browndashptc@gmail.com"; } diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts index ee8afee65..0fdaf07ff 100644 --- a/src/server/DashSession/Session/agents/monitor.ts +++ b/src/server/DashSession/Session/agents/monitor.ts @@ -2,7 +2,7 @@ import { ExitHandler } from "./applied_session_agent"; import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from "../utilities/session_config"; import Repl, { ReplAction } from "../utilities/repl"; import { isWorker, setupMaster, on, Worker, fork } from "cluster"; -import { manage, MessageHandler } from "./promisified_ipc_manager"; +import { manage, MessageHandler, ErrorLike } from "./promisified_ipc_manager"; import { red, cyan, white, yellow, blue } from "colors"; import { exec, ExecOptions } from "child_process"; import { validate, ValidationError } from "jsonschema"; @@ -22,7 +22,7 @@ export class Monitor extends IPCMessageReceiver { private readonly config: Configuration; private activeWorker: Worker | undefined; private key: string | undefined; - // private repl: Repl; + private repl: Repl; public static Create() { if (isWorker) { @@ -46,7 +46,7 @@ export class Monitor extends IPCMessageReceiver { this.configureInternalHandlers(); this.config = this.loadAndValidateConfiguration(); this.initializeClusterFunctions(); - // this.repl = this.initializeRepl(); + this.repl = this.initializeRepl(); } protected configureInternalHandlers = () => { @@ -90,7 +90,7 @@ export class Monitor extends IPCMessageReceiver { } public readonly coreHooks = Object.freeze({ - onCrashDetected: (listener: MessageHandler<{ error: Error }>) => this.on(Monitor.IntrinsicEvents.CrashDetected, listener), + onCrashDetected: (listener: MessageHandler<{ error: ErrorLike }>) => this.on(Monitor.IntrinsicEvents.CrashDetected, listener), onServerRunning: (listener: MessageHandler<{ isFirstTime: boolean }>) => this.on(Monitor.IntrinsicEvents.ServerRunning, listener) }); @@ -119,7 +119,7 @@ export class Monitor extends IPCMessageReceiver { * that can invoke application logic external to this module */ public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { - // this.repl.registerCommand(basename, argPatterns, action); + this.repl.registerCommand(basename, argPatterns, action); } public exec = (command: string, options?: ExecOptions) => { diff --git a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts index feff568e1..95aa686e6 100644 --- a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts +++ b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts @@ -43,8 +43,8 @@ type InternalMessageHandler = (message: InternalMessage) => (any | Promise<any>) * Allows for the transmission of the error's key features over IPC. */ export interface ErrorLike { - name?: string; - message?: string; + name: string; + message: string; stack?: string; } @@ -162,8 +162,8 @@ export class PromisifiedIPCManager { } if (!this.isDestroyed && this.target.send) { const metadata = { id, isResponse: true }; - const response: Response = { results , error }; - const message = { name, args: response , metadata }; + const response: Response = { results, error }; + const message = { name, args: response, metadata }; delete this.pendingMessages[id]; this.target.send(message); } diff --git a/src/server/DashSession/Session/agents/server_worker.ts b/src/server/DashSession/Session/agents/server_worker.ts index 976d27226..6a19bfa5d 100644 --- a/src/server/DashSession/Session/agents/server_worker.ts +++ b/src/server/DashSession/Session/agents/server_worker.ts @@ -1,6 +1,6 @@ import { ExitHandler } from "./applied_session_agent"; import { isMaster } from "cluster"; -import { manage } from "./promisified_ipc_manager"; +import { manage, ErrorLike } from "./promisified_ipc_manager"; import IPCMessageReceiver from "./process_message_router"; import { red, green, white, yellow } from "colors"; import { get } from "request-promise"; @@ -112,7 +112,9 @@ export class ServerWorker extends IPCMessageReceiver { private proactiveUnplannedExit = async (error: Error): Promise<void> => { this.shouldServerBeResponsive = false; // communicates via IPC to the master thread that it should dispatch a crash notification email - this.emit(Monitor.IntrinsicEvents.CrashDetected, { error }); + const { name, message, stack } = error; + const errorLike: ErrorLike = { name, message, stack }; + this.emit(Monitor.IntrinsicEvents.CrashDetected, { error: errorLike }); await this.executeExitHandlers(error); // notify master thread (which will log update in the console) of crash event via IPC this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`)); @@ -157,4 +159,4 @@ export class ServerWorker extends IPCMessageReceiver { this.pollServer(); } -}
\ No newline at end of file +} diff --git a/src/server/DashSession/Session/utilities/session_config.ts b/src/server/DashSession/Session/utilities/session_config.ts index b0e65dde4..bde98e9d2 100644 --- a/src/server/DashSession/Session/utilities/session_config.ts +++ b/src/server/DashSession/Session/utilities/session_config.ts @@ -19,7 +19,7 @@ const identifierProperties: Schema = { const portProperties: Schema = { type: "number", - minimum: 1024, + minimum: 443, maximum: 65535 }; diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index e4d0d1f5f..d9b38c014 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -1,4 +1,4 @@ -import { red } from 'colors'; +import { red, green } from 'colors'; import { ExifImage } from 'exif'; import { File } from 'formidable'; import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs'; @@ -62,6 +62,7 @@ export namespace DashUploadUtils { const category = types[0]; let format = `.${types[1]}`; + console.log(green(`Processing upload of file (${name}) with upload type (${type}) in category (${category}).`)); switch (category) { case "image": diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index a341fd1c2..f1fe582e5 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -8,7 +8,7 @@ export namespace AcceptableMedia { export const webps = [".webp"]; export const tiffs = [".tiff"]; export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs]; - export const videoFormats = [".mov", ".mp4"]; + export const videoFormats = [".mov", ".mp4", ".quicktime"]; export const applicationFormats = [".pdf"]; export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"]; } diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts index 9eb4a328f..3fbd4b3a7 100644 --- a/src/server/authentication/AuthenticationManager.ts +++ b/src/server/authentication/AuthenticationManager.ts @@ -177,13 +177,13 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio const smtpTransport = nodemailer.createTransport({ service: 'Gmail', auth: { - user: 'brownptcdash@gmail.com', - pass: 'browngfx1' + user: 'browndashptc@gmail.com', + pass: 'TsarNicholas#2' } }); const mailOptions = { to: user.email, - from: 'brownptcdash@gmail.com', + from: 'browndashptc@gmail.com', subject: 'Dash Password Reset', text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' + 'Please click on the following link, or paste this into your browser to complete the process:\n\n' + @@ -250,13 +250,13 @@ export let postReset = function (req: Request, res: Response) { const smtpTransport = nodemailer.createTransport({ service: 'Gmail', auth: { - user: 'brownptcdash@gmail.com', - pass: 'browngfx1' + user: 'browndashptc@gmail.com', + pass: 'TsarNicholas#2' } }); const mailOptions = { to: user.email, - from: 'brownptcdash@gmail.com', + from: 'browndashptc@gmail.com', subject: 'Your password has been changed', text: 'Hello,\n\n' + 'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n' diff --git a/src/server/index.ts b/src/server/index.ts index c4e6be8a2..9687c3b23 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -23,6 +23,7 @@ import { Logger } from "./ProcessFactory"; import RouteManager, { Method, PublicHandler } from './RouteManager'; import RouteSubscriber from './RouteSubscriber'; import initializeServer, { resolvedPorts } from './server_Initialization'; +import { DashSessionAgent } from "./DashSession/DashSessionAgent"; export const AdminPriviliges: Map<string, boolean> = new Map(); export const onWindows = process.platform === "win32"; @@ -186,9 +187,9 @@ export async function launchServer() { * log the output of the server process, so it's not ideal for development. * So, the 'else' clause is exactly what we've always run when executing npm start. */ -// if (process.env.RELEASE) { -// (sessionAgent = new DashSessionAgent()).launch(); -// } else { (Database.Instance as Database.Database).doConnect(); -launchServer(); -// } +if (process.env.MONITORED) { + (sessionAgent = new DashSessionAgent()).launch(); +} else { + launchServer(); +} diff --git a/src/server/websocket.ts b/src/server/websocket.ts index e5692a7dd..7d111f359 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -207,9 +207,12 @@ export namespace WebSocket { } } + function GetRefFieldLocal([id, callback]: [string, (result?: Transferable) => void]) { + return Database.Instance.getDocument(id, callback); + } function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { process.stdout.write(`.`); - Database.Instance.getDocument(id, callback); + GetRefFieldLocal([id, callback]); } function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { @@ -275,18 +278,20 @@ export namespace WebSocket { diff.diff.$set = diff.diff.$addToSet; delete diff.diff.$addToSet;// convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const newListItems = diff.diff.$set[updatefield].fields; - const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; - diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; + const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((item: any) => item !== undefined) || []; + diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => newItem && !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; Database.Instance.update(diff.id, diff.diff, () => { if (sendBack) { + console.log("RET BACK"); const id = socket.id; socket.id = ""; socket.broadcast.emit(MessageStore.UpdateField.Message, diff); socket.id = id; } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); + dispatchNextOp(diff.id); }, false); } @@ -301,47 +306,74 @@ export namespace WebSocket { Database.Instance.update(diff.id, diff.diff, () => { if (sendBack) { + console.log("SEND BACK"); const id = socket.id; socket.id = ""; socket.broadcast.emit(MessageStore.UpdateField.Message, diff); socket.id = id; } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); + dispatchNextOp(diff.id); }, false); } + const pendingOps = new Map<string, { diff: Diff, socket: Socket }[]>(); + + function dispatchNextOp(id: string) { + const next = pendingOps.get(id)!.shift(); + if (next) { + const { diff, socket } = next; + if (diff.diff.$addToSet) { + return GetRefFieldLocal([diff.id, (result?: Transferable) => addToListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own + } + if (diff.diff.$remFromSet) { + return GetRefFieldLocal([diff.id, (result?: Transferable) => remFromListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own + } + return GetRefFieldLocal([diff.id, (result?: Transferable) => SetField(socket, diff, result)]); + } + if (!pendingOps.get(id)!.length) pendingOps.delete(id); + } function UpdateField(socket: Socket, diff: Diff) { - if (diff.diff.$addToSet) return GetRefField([diff.id, (result?: Transferable) => addToListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own - if (diff.diff.$remFromSet) return GetRefField([diff.id, (result?: Transferable) => remFromListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own - return GetRefField([diff.id, (result?: Transferable) => SetField(socket, diff, result)]); + if (pendingOps.has(diff.id)) { + pendingOps.get(diff.id)!.push({ diff, socket }); + return true; + } + pendingOps.set(diff.id, [{ diff, socket }]); + if (diff.diff.$addToSet) { + return GetRefFieldLocal([diff.id, (result?: Transferable) => addToListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own + } + if (diff.diff.$remFromSet) { + return GetRefFieldLocal([diff.id, (result?: Transferable) => remFromListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own + } + return GetRefFieldLocal([diff.id, (result?: Transferable) => SetField(socket, diff, result)]); } function SetField(socket: Socket, diff: Diff, curListItems?: Transferable) { Database.Instance.update(diff.id, diff.diff, () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false); const docfield = diff.diff.$set || diff.diff.$unset; - if (!docfield) { - return; - } - const update: any = { id: diff.id }; - let dynfield = false; - for (let key in docfield) { - if (!key.startsWith("fields.")) continue; - dynfield = true; - const val = docfield[key]; - key = key.substring(7); - Object.values(suffixMap).forEach(suf => { update[key + getSuffix(suf)] = { set: null }; }); - const term = ToSearchTerm(val); - if (term !== undefined) { - const { suffix, value } = term; - update[key + suffix] = { set: value }; - if (key.endsWith('lastModified')) { - update["lastModified" + suffix] = value; + if (docfield) { + const update: any = { id: diff.id }; + let dynfield = false; + for (let key in docfield) { + if (!key.startsWith("fields.")) continue; + dynfield = true; + const val = docfield[key]; + key = key.substring(7); + Object.values(suffixMap).forEach(suf => { update[key + getSuffix(suf)] = { set: null }; }); + const term = ToSearchTerm(val); + if (term !== undefined) { + const { suffix, value } = term; + update[key + suffix] = { set: value }; + if (key.endsWith('lastModified')) { + update["lastModified" + suffix] = value; + } } } + if (dynfield) { + Search.updateDocument(update); + } } - if (dynfield) { - Search.updateDocument(update); - } + dispatchNextOp(diff.id); } function DeleteField(socket: Socket, id: string) { diff --git a/startup.sh b/startup.sh new file mode 100644 index 000000000..6554b5666 --- /dev/null +++ b/startup.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd /c/Users/dash/Desktop/Dash-Web # cd /c/Users/dash/Documents/Dash-Web instead for dash-release +npm run start-release +# works for browndash
\ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b06cec79f..f688f18ea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "./src/typings" ], "types": [ - "youtube" + "youtube", + "node" ] }, // "exclude": [ |