diff options
Diffstat (limited to 'src')
138 files changed, 4127 insertions, 3223 deletions
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 d7dfa4498..1d7497cf8 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,6 +1,6 @@ import * as io from 'socket.io-client'; import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message"; -import { Opt, Doc, fetchProto, FieldsSym, UpdatingFromServer } from '../fields/Doc'; +import { Opt, Doc, UpdatingFromServer } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../fields/RefField'; @@ -9,6 +9,8 @@ import { GestureOverlay } from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; import { runInAction } from 'mobx'; import { ObjectField } from '../fields/ObjectField'; +import { StrCast } from '../fields/Types'; +import * as rp from 'request-promise'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -25,6 +27,21 @@ import { ObjectField } from '../fields/ObjectField'; */ export namespace DocServer { let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {}; + + 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))); + }); + print && strings.sort().forEach((str, i) => console.log(i.toString() + " " + str)); + rp.post(Utils.prepend("/setCacheDocumentIds"), { + body: { + cacheDocumentIds: Array.from(Object.keys(_cache)).join(";"), + }, + json: true, + }); + } export let _socket: SocketIOClient.Socket; // this client's distinct GUID created at initialization let GUID: string; @@ -331,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 2d02742c5..2d8a897a5 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,7 +1,8 @@ import { runInAction } from "mobx"; import { basename, extname } from "path"; import { DateField } from "../../fields/DateField"; -import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclAddonly } from "../../fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; import { HtmlField } from "../../fields/HtmlField"; import { InkField } from "../../fields/InkField"; import { List } from "../../fields/List"; @@ -11,6 +12,7 @@ import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../fields/Types"; import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; +import { SharingPermissions } from "../../fields/util"; import { MessageStore } from "../../server/Message"; import { Upload } from "../../server/SharedMediaTypes"; import { OmitKeys, Utils } from "../../Utils"; @@ -33,6 +35,7 @@ import { AudioBox } from "../views/nodes/AudioBox"; import { ColorBox } from "../views/nodes/ColorBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; +import { FilterBox } from "../views/nodes/FilterBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { ImageBox } from "../views/nodes/ImageBox"; @@ -50,9 +53,6 @@ import { PresElementBox } from "../views/presentationview/PresElementBox"; import { SearchBox } from "../views/search/SearchBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { DocumentType } from "./DocumentTypes"; -import { FilterBox } from "../views/nodes/FilterBox"; -import { SharingPermissions } from "../../fields/util"; -import { SharingManager } from "../util/SharingManager"; const path = require('path'); const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); @@ -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; @@ -208,6 +209,8 @@ export interface DocumentOptions { treeViewOutlineMode?: boolean; // whether slide should function as a text outline treeViewLockExpandedView?: boolean; // whether the expanded view can be changed treeViewDefaultExpandedView?: string; // default expanded view + sidebarColor?: string; // background color of text sidebar + sidebarViewType?: string; // collection type of text sidebar limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents // [key: string]: Opt<Field>; pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown @@ -234,6 +237,8 @@ class EmptyBox { export namespace Docs { + export let newAccount: boolean = false; + export namespace Prototypes { type LayoutSource = { LayoutString: (key: string) => string }; @@ -242,6 +247,7 @@ export namespace Docs { view: LayoutSource, dataField: string }, + data?: any, options?: Partial<DocumentOptions> }; type TemplateMap = Map<DocumentType, PrototypeTemplate>; @@ -389,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 => { @@ -462,6 +468,7 @@ export namespace Docs { const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) }; options.layout = layout.view?.LayoutString(layout.dataField); const doc = Doc.assign(new Doc(prototypeId, true), { system: true, layoutKey: "layout", ...options }); + doc.data = template.data; doc.layout_keyValue = KeyValueBox.LayoutString(""); return doc; } @@ -572,7 +579,7 @@ export namespace Docs { * only when creating a DockDocument from the current user's already existing * main document. */ - export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data") { + export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data", protoId?: string) { const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys, "^_"); protoProps.system = delegateProps.system; @@ -588,7 +595,7 @@ export namespace Docs { protoProps.isPrototype = true; - const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey); + const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey, protoId); const viewDoc = Doc.MakeDelegate(dataDoc, delegId); // so that the list of annotations is already initialised, prevents issues in addonly. @@ -603,6 +610,7 @@ export namespace Docs { viewDoc.type !== DocumentType.LINK && DocUtils.MakeLinkToActiveAudio(viewDoc); viewDoc["acl-Public"] = dataDoc["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + viewDoc["acl-Override"] = dataDoc["acl-Override"] = "None"; return Doc.assign(viewDoc, delegateProps, true); } @@ -617,8 +625,8 @@ export namespace Docs { * @param options initial values to apply to this new delegate * @param value the data to store in this new delegate */ - function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D, fieldKey: string = "data") { - const deleg = Doc.MakeDelegate(proto); + function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D, fieldKey: string = "data", id: string | undefined = undefined) { + const deleg = Doc.MakeDelegate(proto, id); if (value !== undefined) { deleg[fieldKey] = value; } @@ -720,8 +728,8 @@ export namespace Docs { LinkManager.Instance.addLink(doc); - Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)"); - Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)"); + source.doc.links === undefined && (Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)")); + target.doc.links === undefined && (Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)")); return doc; } @@ -747,6 +755,8 @@ export namespace Docs { I.author = Doc.CurrentUserEmail; I.rotation = 0; I.data = new InkField(points); + I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + I["acl-Override"] = "None"; return I; } @@ -771,7 +781,7 @@ export namespace Docs { } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Freeform }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Freeform }, id); } export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { @@ -802,8 +812,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Tree }, id); } - export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Stacking }, id); + export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId); } export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) { @@ -815,7 +825,7 @@ export namespace Docs { export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Masonry }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Masonry }); } export function LabelDocument(options?: DocumentOptions) { @@ -886,17 +896,44 @@ export namespace Docs { } export namespace DocUtils { + export function Excluded(d: Doc, docFilters: string[]) { + const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields + docFilters.forEach(filter => { + const fields = filter.split(":"); + const key = fields[0]; + const value = fields[1]; + const modifiers = fields[2]; + if (!filterFacets[key]) { + filterFacets[key] = {}; + } + filterFacets[key][value] = modifiers; + }); + + if (d.z) return false; + for (const facetKey of Object.keys(filterFacets)) { + const facet = filterFacets[facetKey]; + const xs = Object.keys(facet).filter(value => facet[value] === "x"); + const failsNotEqualFacets = xs?.some(value => Doc.matchFieldValue(d, facetKey, value)); + if (failsNotEqualFacets) { + return true; + } + } + return false; + } export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField) { const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - for (let i = 0; i < docFilters.length; i += 3) { - const [key, value, modifiers] = docFilters.slice(i, i + 3); + docFilters.forEach(filter => { + const fields = filter.split(":"); + const key = fields[0]; + const value = fields[1]; + const modifiers = fields[2]; if (!filterFacets[key]) { filterFacets[key] = {}; } filterFacets[key][value] = modifiers; - } + }); const filteredDocs = docFilters.length ? childDocs.filter(d => { if (d.z) return true; @@ -928,7 +965,7 @@ export namespace DocUtils { const min = Number(docRangeFilters[i + 1]); const max = Number(docRangeFilters[i + 2]); const val = Cast(d[key], "number", null); - if (val !== undefined && (val < min || val > max)) { + if (val === undefined || (val < min || val > max)) { return false; } } @@ -974,9 +1011,9 @@ export namespace DocUtils { DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d }, "audio link", "audio timeline")); } - export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string) { + export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean) { const sv = DocumentManager.Instance.getDocumentView(source.doc); - if (sv && sv.props.ContainingCollectionDoc === target.doc) return; + if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return; if (target.doc === Doc.UserDoc()) return undefined; const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView", description }, id); @@ -1065,13 +1102,13 @@ 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; } - export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void { - ContextMenu.Instance.addItem({ + export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false): void { + !simpleMenu && ContextMenu.Instance.addItem({ description: "Add Note ...", subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({ description: ":" + StrCast(note.title), @@ -1090,14 +1127,15 @@ export namespace DocUtils { }); ContextMenu.Instance.addItem({ description: "Add Template Doc ...", - subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({ + subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({ description: ":" + StrCast(dragDoc.title), event: undoBatch((args: { x: number, y: number }) => { - const newDoc = Doc.ApplyTemplate(dragDoc); + const newDoc = Doc.copyDragFactory(dragDoc); if (newDoc) { newDoc.author = Doc.CurrentUserEmail; newDoc.x = x; newDoc.y = y; + if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id]; docAdder(newDoc); } }), diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 342954a4e..4f054269f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,6 +1,6 @@ import { computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; -import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; @@ -8,12 +8,14 @@ import { RichTextField } from "../../fields/RichTextField"; import { listSpec } from "../../fields/Schema"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types"; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast, DateCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; +import { SharingPermissions } from "../../fields/util"; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; import { DocumentType } from "../documents/DocumentTypes"; +import { Networking } from "../Network"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; @@ -30,8 +32,10 @@ import { Scripting } from "./Scripting"; import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; import { UndoManager } from "./UndoManager"; +import { SnappingManager } from "./SnappingManager"; +export let resolvedPorts: { server: number, socket: number }; const headerViewVersion = "0.1"; export class CurrentUserUtils { private static curr_id: string; @@ -228,7 +232,7 @@ export class CurrentUserUtils { } else { const curButnTypes = Cast(doc["template-buttons"], Doc, null); DocListCastAsync(curButnTypes.data).then(async curBtns => { - await Promise.all(curBtns!); + curBtns && await Promise.all(curBtns); requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype)); }); } @@ -277,7 +281,7 @@ export class CurrentUserUtils { const curNoteTypes = Cast(doc["template-notes"], Doc, null); const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc];//, doc["template-note-Todo"] as any as Doc]; DocListCastAsync(curNoteTypes.data).then(async curNotes => { - await Promise.all(curNotes!); + curNotes && await Promise.all(curNotes); requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype)); }); } @@ -348,7 +352,7 @@ export class CurrentUserUtils { const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc, doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; DocListCastAsync(templateIconsDoc.data).then(async curIcons => { - await Promise.all(curIcons!); + curIcons && await Promise.all(curIcons); requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); }); } @@ -401,10 +405,10 @@ 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.userColor}' />" + + " <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}' />" + " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" + "</div>"; (headerTemplate.proto as Doc).isTemplateDoc = makeTemplate(headerTemplate.proto as Doc, true, "headerView"); @@ -422,9 +426,13 @@ export class CurrentUserUtils { doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyAudio === undefined) { - doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "ready to record audio", system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); ((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0; } + if (doc.emptyNote === undefined) { + doc.emptyNote = Docs.Create.TextDocument("", { _width: 200, title: "text note", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyNote as Doc).proto as Doc)["dragFactory-count"] = 0; + } if (doc.emptyImage === undefined) { doc.emptyImage = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth: 250, title: "an image of a cat", system: true }); } @@ -444,6 +452,7 @@ export class CurrentUserUtils { this.setupActiveMobileMenu(doc); } return [ + { toolTip: "Tap to create a note in a new pane, drag for a note", title: "Note", icon: "sticky-note", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyNote as Doc, noviceMode: true, clickFactory: doc.emptyNote as Doc, }, { toolTip: "Tap to create a collection in a new pane, drag for a collection", title: "Col", icon: "folder", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyCollection as Doc, noviceMode: true, clickFactory: doc.emptyPane as Doc, }, { toolTip: "Tap to create a webpage in a new pane, drag for a webpage", title: "Web", icon: "globe-asia", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true }, { toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc, noviceMode: true }, @@ -508,10 +517,7 @@ export class CurrentUserUtils { return doc.myItemCreators as Doc; } - static menuBtnDescriptions(doc: Doc): { - title: string, target: Doc, icon: string, click: string, watchedDocuments?: Doc - }[] { - this.setupSharingSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing + static async menuBtnDescriptions(doc: Doc) { return [ { title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' }, { title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' }, @@ -535,9 +541,10 @@ export class CurrentUserUtils { })) as any as Doc; } } - static setupMenuPanel(doc: Doc) { + static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { if (doc.menuStack === undefined) { - const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, watchedDocuments }) => + await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing + const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments }) => Docs.Create.FontIconDocument({ icon, iconShape: "square", @@ -561,7 +568,7 @@ export class CurrentUserUtils { title: "menuItemPanel", childDropAction: "alias", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - _backgroundColor: "black", + _backgroundColor: "black", ignoreClick: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, lockedPosition: true, _chromeStatus: "disabled", system: true @@ -724,7 +731,7 @@ export class CurrentUserUtils { if (doc.myTools === undefined) { const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { - title: "My Tools", _width: 500, _yMargin: 20, lockedPosition: true, _chromeStatus: "disabled", forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true, + title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true, })) as any as Doc; doc.myTools = toolsStack; @@ -738,7 +745,7 @@ export class CurrentUserUtils { doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "My Dashboards", _height: 400, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })); const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); @@ -754,7 +761,7 @@ export class CurrentUserUtils { doc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "My Presentations", _height: 100, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })); const newPresentations = ScriptField.MakeScript(`createNewPresentation()`); @@ -772,7 +779,7 @@ export class CurrentUserUtils { doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "Recently Closed", _height: 500, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })); const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); @@ -787,7 +794,7 @@ export class CurrentUserUtils { doc.myFilter = new PrefetchProxy(Docs.Create.FilterDocument({ title: "FilterDoc", _height: 500, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "none", - treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })); const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); @@ -803,7 +810,7 @@ export class CurrentUserUtils { doc.treeViewExpandedView = "fields"; doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], { treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, title: "My UserDoc", - treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })) as any as Doc; } @@ -869,9 +876,30 @@ export class CurrentUserUtils { } // Sharing sidebar is where shared documents are contained - static setupSharingSidebar(doc: Doc) { + static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { + if (doc.myLinkDatabase === undefined) { + let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId); + if (!linkDocs) { + linkDocs = new Doc(linkDatabaseId, true); + (linkDocs as Doc).author = Doc.CurrentUserEmail; + (linkDocs as Doc).data = new List<Doc>([]); + (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add; + } + doc.myLinkDatabase = new PrefetchProxy(linkDocs); + } if (doc.mySharedDocs === undefined) { - doc.mySharedDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, _showTitle: "title", ignoreClick: true, lockedPosition: true })); + 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, + _showTitle: "title", ignoreClick: true, lockedPosition: true, + }, sharingDocumentId + "outer", sharingDocumentId); + (sharedDocs as Doc)["acl-Public"] = Doc.GetProto(sharedDocs as Doc)["acl-Public"] = SharingPermissions.Add; + } + if (sharedDocs instanceof Doc) { + sharedDocs.userColor = sharedDocs.userColor || "rgb(202, 202, 202)"; + } + doc.mySharedDocs = new PrefetchProxy(sharedDocs); } } @@ -938,11 +966,18 @@ export class CurrentUserUtils { return doc.clickFuncs as Doc; } - static async updateUserDocument(doc: Doc) { + static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { + if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); + const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); + reaction(() => DateCast((doc.globalGroupDatabase as Doc).lastModified), + async () => { + const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); + const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; + SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); + }, { fireImmediately: true }); doc.system = true; doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; - doc.userColor = doc.userColor || "#12121233"; doc._raiseWhenDragged = true; doc.activeInkPen = doc; doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); @@ -971,10 +1006,8 @@ export class CurrentUserUtils { this.setupOverlays(doc); // documents in overlay layer this.setupDockedButtons(doc); // the bottom bar of font icons await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels - this.setupMenuPanel(doc); - doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument(); - doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); - doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); + await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId); + if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered @@ -991,13 +1024,18 @@ export class CurrentUserUtils { // Doc.AddDocToList(Cast(doc["template-notes"], Doc, null), "data", deleg); // } // }); + setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500); return doc; } public static async loadCurrentUser() { - return rp.get(Utils.prepend("/getCurrentUser")).then(response => { + return rp.get(Utils.prepend("/getCurrentUser")).then(async response => { if (response) { - const result: { id: string, email: string } = JSON.parse(response); + const result: { id: string, email: string, cacheDocumentIds: string } = JSON.parse(response); + Doc.CurrentUserEmail = result.email; + resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts")); + DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email); + result.cacheDocumentIds && (await DocServer.GetRefFields(result.cacheDocumentIds.split(";"))); return result; } else { throw new Error("There should be a user! Why does Dash think there isn't one?"); @@ -1005,13 +1043,17 @@ export class CurrentUserUtils { }); } - public static async loadUserDocument({ id, email }: { id: string, email: string }) { + public static async loadUserDocument(id: string) { this.curr_id = id; - Doc.CurrentUserEmail = email; - await rp.get(Utils.prepend("/getUserDocumentId")).then(id => { - if (id && id !== "guest") { - return DocServer.GetRefField(id).then(async field => - Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true)))); + 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 => { + 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?"); } @@ -1071,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. @@ -1082,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(); }); @@ -1127,8 +1172,9 @@ export class CurrentUserUtils { CurrentUserUtils.openDashboard(userDoc, dashboardDoc); } - public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number) { + public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean) { const tbox = Docs.Create.TextDocument("", { + _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, _width: width || 200, _height: height || 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), _fontFamily: StrCast(Doc.UserDoc().fontFamily), title }); @@ -1147,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) { @@ -1165,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 c3d78a028..a6816c7f9 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,16 +1,17 @@ -import { action, computed, observable } from 'mobx'; -import { Doc, DocListCastAsync, DocListCast, Opt } from '../../fields/Doc'; +import { action, observable } from 'mobx'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { returnFalse } from '../../Utils'; +import { DocumentType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; -import { DocumentView, DocFocusFunc } from '../views/nodes/DocumentView'; +import { DocumentView } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; import { Scripting } from './Scripting'; import { SelectionManager } from './SelectionManager'; -import { DocumentType } from '../documents/DocumentTypes'; -import { TraceMobx } from '../../fields/util'; -import { returnFalse } from '../../Utils'; +import { LinkDocPreview } from '../views/nodes/LinkDocPreview'; +import { FormattedTextBoxComment } from '../views/nodes/formattedText/FormattedTextBoxComment'; export type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; @@ -19,6 +20,7 @@ export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @observable public DocumentViews: DocumentView[] = []; + @observable LinkedDocumentViews: { a: DocumentView, b: DocumentView, l: Doc }[] = []; // singleton instance private static _instance: DocumentManager; @@ -32,6 +34,26 @@ export class DocumentManager { private constructor() { } + @action + public AddView = (view: DocumentView) => { + const linksList = DocListCast(view.props.Document.links); + linksList.forEach(link => { + const linkToDoc = link && LinkManager.getOppositeAnchor(link, view.props.Document); + linkToDoc && DocumentManager.Instance.DocumentViews.filter(dv => Doc.AreProtosEqual(dv.props.Document, linkToDoc)).forEach(dv => { + if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== view.props.LayoutTemplateString) { + this.LinkedDocumentViews.push({ a: dv, b: view, l: link }); + } + }); + }); + this.DocumentViews.push(view); + } + public RemoveView = (view: DocumentView) => { + const index = this.DocumentViews.indexOf(view); + index !== -1 && this.DocumentViews.splice(index, 1); + + this.LinkedDocumentViews.slice().forEach(action((pair, i) => pair.a === view || pair.b === view ? this.LinkedDocumentViews.splice(i, 1) : null)); + } + //gets all views public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; @@ -88,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[] = []; @@ -105,39 +127,20 @@ export class DocumentManager { return toReturn; } - @computed - public get LinkedDocumentViews() { - TraceMobx(); - const pairs = DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { - const linksList = DocListCast(dv.props.Document.links); - pairs.push(...linksList.reduce((pairs, link) => { - const linkToDoc = link && LinkManager.getOppositeAnchor(link, dv.props.Document); - linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { - if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== docView1.props.LayoutTemplateString) { - pairs.push({ a: dv, b: docView1, l: link }); - } - }); - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); - return pairs; - } - - 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; }; @@ -150,7 +153,7 @@ export class DocumentManager { }; const docView = getFirstDocView(targetDoc, originatingDoc); let annotatedDoc = await Cast(targetDoc.annotationOn, Doc); - if (annotatedDoc && !targetDoc?.isPushpin) { + if (annotatedDoc && annotatedDoc !== originatingDoc?.context && !targetDoc?.isPushpin) { const first = getFirstDocView(annotatedDoc); if (first) { annotatedDoc = first.props.Document; @@ -158,15 +161,23 @@ 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) { - docView.props.Document.hidden = !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(); + }, 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); - docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish); - highlight(); + // @ts-ignore + docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish, sameContext, false); } + highlight(); } else { const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined; @@ -177,7 +188,7 @@ export class DocumentManager { highlight(); } else { // otherwise try to get a view of the context of the target const targetDocContextView = getFirstDocView(targetDocContext); - targetDocContext._scrollY = NumCast(targetDocContext._scrollTop, 0); // this will force PDFs to activate and load their annotations / allow scrolling + targetDocContext._scrollY = targetDocContext._scrollPreviewY = NumCast(targetDocContext._scrollTop, 0); // this will force PDFs to activate and load their annotations / allow scrolling if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. targetDocContext._viewTransition = "transform 500ms"; targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom); @@ -192,34 +203,31 @@ export class DocumentManager { if (retryDocView) { // we found the target in the context retryDocView.props.focus(targetDoc, willZoom, undefined, focusAndFinish); // focus on the target in the context highlight(); - } - if (delay > 2500) { + } else if (delay > 1500) { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.props.Document); - // targetDoc.layout && createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target + if (targetDoc.layout) { + Doc.SetInPlace(targetDoc, "annotationOn", undefined, false); + createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target + } } else { setTimeout(() => findView(delay + 250), 250); } }; findView(0); } - } else { // there's no context view so we need to create one first and try again - createViewFunc(targetDocContext); // so first we create the target, but don't pass finished because we still need to create the target - setTimeout(() => { - const finalDocView = getFirstDocView(targetDoc); - const finalDocContextView = getFirstDocView(targetDocContext); - setTimeout(() => // if not, wait a bit to see if the context can be loaded (e.g., a PDF). wait interval heurisitic tries to guess how we're animating based on what's just become visible - this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkDoc, true, undefined, finished), // pass true this time for closeContextIfNotFound - finalDocView ? 0 : finalDocContextView ? 250 : 2000); // so call jump to doc again and if the doc isn't found, it will be created. - }, 0); + } else { // there's no context view so we need to create one first and try again when that finishes + createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target + () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished)); } } } } public async FollowLink(link: Opt<Doc>, doc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { + LinkDocPreview.TargetDoc = undefined; + FormattedTextBoxComment.linkDoc = undefined; const linkDocs = link ? [link] : DocListCast(doc.links); - SelectionManager.DeselectAll(); const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1 const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2 const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 91ffab41d..86e2d339e 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -129,6 +129,7 @@ export namespace DragManager { treeViewDoc?: Doc; dontHideOnDrop?: boolean; offset: number[]; + canEmbed?: boolean; userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move' @@ -200,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); @@ -208,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 = @@ -324,7 +333,7 @@ export namespace DragManager { if (dragData.dropAction === "none") return; const batch = UndoManager.StartBatch("dragging"); eles = eles.filter(e => e); - CanEmbed = false; + CanEmbed = dragData.canEmbed || false; if (!dragDiv) { dragDiv = document.createElement("div"); dragDiv.className = "dragManager-dragDiv"; @@ -339,7 +348,6 @@ export namespace DragManager { DragManager.Root().appendChild(dragDiv); } dragLabel.style.display = ""; - SnappingManager.SetIsDragging(true); const scaleXs: number[] = []; const scaleYs: number[] = []; const xs: number[] = []; @@ -408,8 +416,15 @@ 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; let lastY = downY; const xFromLeft = downX - elesCont.left; @@ -505,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/GroupManager.tsx b/src/client/util/GroupManager.tsx index cb15b5081..6458de0ed 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -5,15 +5,15 @@ import * as React from "react"; import Select from 'react-select'; import * as RequestPromise from "request-promise"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; -import { Cast, StrCast } from "../../fields/Types"; -import { setGroups } from "../../fields/util"; +import { StrCast, Cast } from "../../fields/Types"; import { Utils } from "../../Utils"; -import { DocServer } from "../DocServer"; import { MainViewModal } from "../views/MainViewModal"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; import "./GroupManager.scss"; import { GroupMemberView } from "./GroupMemberView"; import { SharingManager, User } from "./SharingManager"; +import { listSpec } from "../../fields/Schema"; +import { DateField } from "../../fields/DateField"; /** * Interface for options for the react-select component @@ -34,62 +34,23 @@ export class GroupManager extends React.Component<{}> { @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button - private currentUserGroups: string[] = []; // the list of groups the current user is a member of @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; - private populating: boolean = false; - - constructor(props: Readonly<{}>) { super(props); GroupManager.Instance = this; } - /** - * Populates the list of users and groups. - */ - componentDidMount() { - this.populateUsers(); - this.populateGroups(); - } + componentDidMount() { this.populateUsers(); } /** * Fetches the list of users stored on the database. */ populateUsers = async () => { - if (!this.populating) { - this.populating = true; - runInAction(() => this.users = []); - const userList = await RequestPromise.get(Utils.prepend("/getUsers")); - const raw = JSON.parse(userList) as User[]; - const evaluating = raw.map(async user => { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument.mySharedDocs, Doc); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push(user.email); - } - }); - } - }); - return Promise.all(evaluating).then(() => this.populating = false); - } - } - - /** - * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts - */ - populateGroups = () => { - DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { - groups?.forEach(group => { - const members: string[] = JSON.parse(StrCast(group.members)); - if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); - }); - this.currentUserGroups.push("Public"); - setGroups(this.currentUserGroups); - }); + const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const raw = JSON.parse(userList) as User[]; + raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email))); } /** @@ -107,7 +68,6 @@ export class GroupManager extends React.Component<{}> { // SelectionManager.DeselectAll(); this.isOpen = true; this.populateUsers(); - this.populateGroups(); } /** @@ -126,25 +86,24 @@ export class GroupManager extends React.Component<{}> { /** * @returns the database of groups. */ - get GroupManagerDoc(): Doc | undefined { - return Doc.UserDoc().globalGroupDatabase as Doc; - } + @computed get GroupManagerDoc(): Doc | undefined { return Doc.UserDoc().globalGroupDatabase as Doc; } /** * @returns a list of all group documents. */ - getAllGroups(): Doc[] { - const groupDoc = this.GroupManagerDoc; - return groupDoc ? DocListCast(groupDoc.data) : []; - } + @computed get allGroups(): Doc[] { return DocListCast(this.GroupManagerDoc?.data); } + + /** + * @returns the members of the admin group. + */ + @computed get adminGroupMembers(): string[] { return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; } /** * @returns a group document based on the group name. * @param groupName */ getGroup(groupName: string): Doc | undefined { - const groupDoc = this.getAllGroups().find(group => group.groupName === groupName); - return groupDoc; + return this.allGroups.find(group => group.title === groupName); } /** @@ -152,15 +111,9 @@ export class GroupManager extends React.Component<{}> { */ getGroupMembers(group: string | Doc): string[] { if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[]; - else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; + return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; } - /** - * @returns the members of the admin group. - */ - get adminGroupMembers(): string[] { - return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; - } /** * @returns a boolean indicating whether the current user has access to edit group documents. @@ -178,14 +131,11 @@ export class GroupManager extends React.Component<{}> { * @param memberEmails */ createGroupDoc(groupName: string, memberEmails: string[] = []) { - const groupDoc = new Doc; - groupDoc.groupName = groupName.toLowerCase() === "admin" ? "Admin" : groupName; + const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName; + const groupDoc = new Doc("GROUP:" + name, true); + groupDoc.title = name; groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]); groupDoc.members = JSON.stringify(memberEmails); - if (memberEmails.includes(Doc.CurrentUserEmail)) { - this.currentUserGroups.push(groupName); - setGroups(this.currentUserGroups); - } this.addGroup(groupDoc); } @@ -196,6 +146,7 @@ export class GroupManager extends React.Component<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc); + this.GroupManagerDoc.lastModified = new DateField; return true; } return false; @@ -205,19 +156,20 @@ export class GroupManager extends React.Component<{}> { * Deletes a group from the database of group documents and @returns whether the group was deleted or not. * @param group */ + @action deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); SharingManager.Instance.removeGroup(group); - const members: string[] = JSON.parse(StrCast(group.members)); + const members = JSON.parse(StrCast(group.members)); if (members.includes(Doc.CurrentUserEmail)) { - const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName); - index !== -1 && this.currentUserGroups.splice(index, 1); - setGroups(this.currentUserGroups); + const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group); + index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1); } + this.GroupManagerDoc.lastModified = new DateField; if (group === this.currentGroup) { - runInAction(() => this.currentGroup = undefined); + this.currentGroup = undefined; } return true; } @@ -232,10 +184,11 @@ export class GroupManager extends React.Component<{}> { */ addMemberToGroup(groupDoc: Doc, email: string) { if (this.hasEditAccess(groupDoc)) { - const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); + const memberList = JSON.parse(StrCast(groupDoc.members)); !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); } } @@ -246,12 +199,13 @@ export class GroupManager extends React.Component<{}> { */ removeMemberFromGroup(groupDoc: Doc, email: string) { if (this.hasEditAccess(groupDoc)) { - const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); + const memberList = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); if (index !== -1) { const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); + this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); } } } @@ -278,21 +232,24 @@ export class GroupManager extends React.Component<{}> { */ @action createGroup = () => { - if (!this.inputRef.current?.value) { + const { value } = this.inputRef.current!; + if (!value) { alert("Please enter a group name"); return; } - if (this.inputRef.current.value.toLowerCase() === "admin" && this.getGroup("Admin")) { - alert("You cannot override the Admin group"); - return; + if (["admin", "public", "override"].includes(value.toLowerCase())) { + if (value.toLowerCase() !== "admin" || (value.toLowerCase() === "admin" && this.getGroup("Admin"))) { + alert(`You cannot override the ${value.charAt(0).toUpperCase() + value.slice(1)} group`); + return; + } } - if (this.getGroup(this.inputRef.current.value)) { + if (this.getGroup(value)) { alert("Please select a unique group name"); return; } - this.createGroupDoc(this.inputRef.current.value, this.selectedUsers?.map(user => user.value)); + this.createGroupDoc(value, this.selectedUsers?.map(user => user.value)); this.selectedUsers = null; - this.inputRef.current.value = ""; + this.inputRef.current!.value = ""; this.buttonColour = "#979797"; const { left, width, top } = this.createGroupButtonRef.current!.getBoundingClientRect(); @@ -378,14 +335,13 @@ export class GroupManager extends React.Component<{}> { private get groupInterface() { const sortGroups = (d1: Doc, d2: Doc) => { - const g1 = StrCast(d1.groupName); - const g2 = StrCast(d2.groupName); + const g1 = StrCast(d1.title); + const g2 = StrCast(d2.title); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; }; - let groups = this.getAllGroups(); - groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups; + const groups = this.groupSort === "ascending" ? this.allGroups.sort(sortGroups) : this.groupSort === "descending" ? this.allGroups.sort(sortGroups).reverse() : this.allGroups; return ( <div className="group-interface"> @@ -399,7 +355,7 @@ export class GroupManager extends React.Component<{}> { <div className="group-heading"> <p><b>Manage Groups</b></p> <button onClick={action(() => this.createGroupModalOpen = true)}> - <FontAwesomeIcon icon={"plus-hexagon"} size={"sm"} /> Create Group + <FontAwesomeIcon icon={"plus"} size={"sm"} /> Create Group </button> <div className={"close-button"} onClick={this.close}> <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> @@ -418,9 +374,9 @@ export class GroupManager extends React.Component<{}> { {groups.map(group => <div className="group-row" - key={StrCast(group.groupName)} + key={StrCast(group.title || group.groupName)} > - <div className="group-name" >{group.groupName}</div> + <div className="group-name" >{group.title || group.groupName}</div> <div className="group-info" onClick={action(() => this.currentGroup = group)}> <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> </div> @@ -434,16 +390,13 @@ export class GroupManager extends React.Component<{}> { } render() { - return ( - <MainViewModal - contents={this.groupInterface} - isDisplayed={this.isOpen} - interactive={true} - dialogueBoxStyle={{ zIndex: 1002 }} - overlayStyle={{ zIndex: 1001 }} - closeOnExternalClick={this.close} - /> - ); + return <MainViewModal + contents={this.groupInterface} + isDisplayed={this.isOpen} + interactive={true} + dialogueBoxStyle={{ zIndex: 1002 }} + overlayStyle={{ zIndex: 1001 }} + closeOnExternalClick={this.close} + />; } - }
\ No newline at end of file diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index 4ead01e9f..927200ed3 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -33,8 +33,8 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> { <input className="group-title" style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }} - value={StrCast(this.props.group.groupName)} - onChange={e => this.props.group.groupName = e.currentTarget.value} + value={StrCast(this.props.group.title || this.props.group.groupName)} + onChange={e => this.props.group.title = e.currentTarget.value} disabled={!hasEditAccess} > </input> 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 4f3cfcd03..802b8ae7b 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,8 +1,7 @@ import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { listSpec } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; -import { CurrentUserUtils } from "./CurrentUserUtils"; +import { SharingManager } from "./SharingManager"; +import { computedFn } from "mobx-utils"; /* * link doc: @@ -23,181 +22,58 @@ import { CurrentUserUtils } from "./CurrentUserUtils"; export class LinkManager { private static _instance: LinkManager; - public static currentLink: Opt<Doc>; - - public static get Instance(): LinkManager { - return this._instance || (this._instance = new this()); - } - - private constructor() { - } - - // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes' - // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type - public get LinkManagerDoc(): Doc | undefined { - return Doc.UserDoc().globalLinkDatabase as Doc; - } - - public getAllLinks(): Doc[] { - const ldoc = LinkManager.Instance.LinkManagerDoc; - return ldoc ? DocListCast(ldoc.data) : []; - } - - public addLink(linkDoc: Doc): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - Doc.AddDocToList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc); - return true; - } - return false; - } - - public deleteLink(linkDoc: Doc): boolean { - if (LinkManager.Instance.LinkManagerDoc && linkDoc instanceof Doc) { - Doc.RemoveDocFromList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc); - return true; - } - return false; - } - - // 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?.title !== undefined && a2?.title !== undefined) || link.author === Doc.CurrentUserEmail) && (protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor)); - }); - return related; - } - // finds all links that contain the given anchor - public getAllRelatedLinks(anchor: Doc): Doc[] { - const related = LinkManager.Instance.getAllDirectLinks(anchor); - DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).map(anno => { - related.push(...LinkManager.Instance.getAllRelatedLinks(anno)); + public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); } + + 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(); } + + 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).forEach(doc => lset.add(doc))); + return Array.from(lset); + }, true); + + 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; - } - - public deleteAllLinksOnAnchor(anchor: Doc) { - const related = LinkManager.Instance.getAllRelatedLinks(anchor); - related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); - } - - public addGroupType(groupType: string): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]); - const groupTypes = LinkManager.Instance.getAllGroupTypes(); - groupTypes.push(groupType); - LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); - return true; - } - return false; - } + }, true); - // removes all group docs from all links with the given group type - public deleteGroupType(groupType: string): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - if (LinkManager.Instance.LinkManagerDoc[groupType]) { - const groupTypes = LinkManager.Instance.getAllGroupTypes(); - const index = groupTypes.findIndex(type => type.toUpperCase() === groupType.toUpperCase()); - if (index > -1) groupTypes.splice(index, 1); - LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); - LinkManager.Instance.LinkManagerDoc[groupType] = undefined; - LinkManager.Instance.getAllLinks().forEach(async linkDoc => { - const anchor1 = await Cast(linkDoc.anchor1, Doc); - const anchor2 = await Cast(linkDoc.anchor2, Doc); - anchor1 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor1, groupType); - anchor2 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor2, groupType); - }); - } - return true; - } else return false; - } - - public getAllGroupTypes(): string[] { - if (LinkManager.Instance.LinkManagerDoc) { - if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) { - return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []); - } else { - LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]); - return []; - } - } - return []; - } - - // 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; } - // gets a list of strings representing the keys of the metadata associated with the given group type - public getMetadataKeysInGroup(groupType: string): string[] { - if (LinkManager.Instance.LinkManagerDoc) { - return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : []; - } - return []; - } - - public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys); - return true; - } - return false; - } - - // 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 @@ -208,6 +84,8 @@ export class LinkManager { const a2 = Cast(linkDoc.anchor2, Doc, null); if (Doc.AreProtosEqual(anchor, a1)) return a2; if (Doc.AreProtosEqual(anchor, a2)) return a1; + if (Doc.AreProtosEqual(anchor, a1.annotationOn as Doc)) return a2; + if (Doc.AreProtosEqual(anchor, a2.annotationOn as Doc)) return a1; if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } }
\ No newline at end of file diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index b09eff849..79759a71d 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -42,8 +42,9 @@ export namespace SearchUtil { let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}*:* AND ${arg}`); if (options.onlyAliases) { const header = query.match(/_[atnb]?:/) ? replacedQuery : "DEFAULT:" + replacedQuery; - replacedQuery = `{!join from=id to=proto_i}${header}`; + replacedQuery = `{!join from=id to=proto_i}* AND ${header}`; } + //console.log("Q: " + replacedQuery + " fq: " + options.fq); const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } }); const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten); if (!returnDocs) { @@ -59,12 +60,14 @@ export namespace SearchUtil { const fileids = txtresult ? txtresult.ids : []; const newIds: string[] = []; const newLines: string[][] = []; - await Promise.all(fileids.map(async (tr: string, i: number) => { - const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query - const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } })); - newIds.push(...docResult.ids); - newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i])); - })); + if (fileids) { + await Promise.all(fileids.map(async (tr: string, i: number) => { + const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query + const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } })); + newIds.push(...docResult.ids); + newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i])); + })); + } const theDocs: Doc[] = []; @@ -91,7 +94,7 @@ export namespace SearchUtil { } } - return { docs: theDocs, numFound: result.numFound, highlighting, lines: theLines }; + return { docs: theDocs, numFound: Math.max(0, result.numFound), highlighting, lines: theLines }; } export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 008ce281c..34e88c7b0 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -2,7 +2,6 @@ import { observable, action, runInAction, ObservableMap } from "mobx"; import { Doc, Opt } from "../../fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { computedFn } from "mobx-utils"; -import { List } from "../../fields/List"; import { CollectionSchemaView } from "../views/collections/CollectionSchemaView"; import { CollectionViewType } from "../views/collections/CollectionView"; @@ -67,15 +66,16 @@ export namespace SelectionManager { manager.SelectSchemaDoc(colSchema, document); } + const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed + return manager.SelectedDocuments.get(doc) ? true : false; + }); // computed functions, such as used in IsSelected generate errors if they're called outside of a // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature // to avoid unnecessary mobx invalidations when running inside a reaction. export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean { return !doc ? false : outsideReaction ? manager.SelectedDocuments.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get() - computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed - return manager.SelectedDocuments.get(doc) ? true : false; - })(doc); + IsSelectedCache(doc); } export function DeselectAll(except?: Doc): void { diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 19b217726..00ac6e521 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -43,7 +43,7 @@ export namespace SerializationHelper { } if (!obj.__type) { - if (ClientUtils.RELEASE) { + if (true || ClientUtils.RELEASE) { console.warn("No property 'type' found in JSON."); return undefined; } else { diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 17e93ad17..9934f26d3 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { ColorState, SketchPicker } from "react-color"; import { Doc } from "../../fields/Doc"; -import { BoolCast, StrCast } from "../../fields/Types"; +import { BoolCast, StrCast, Cast } from "../../fields/Types"; import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils"; import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager"; import { DocServer } from "../DocServer"; @@ -56,7 +56,7 @@ export class SettingsManager extends React.Component<{}> { @undoBatch changeFontFamily = action((e: React.ChangeEvent) => Doc.UserDoc().fontFamily = (e.currentTarget as any).value); @undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value); @undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex)); - @undoBatch switchUserColor = action((color: ColorState) => Doc.UserDoc().userColor = String(color.hex)); + @undoBatch switchUserColor = action((color: ColorState) => Doc.SharingDoc().userColor = String(color.hex)); @undoBatch playgroundModeToggle = action(() => { this.playgroundMode = !this.playgroundMode; @@ -205,6 +205,6 @@ export class SettingsManager extends React.Component<{}> { isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: "600px" }} />; + dialogueBoxStyle={{ width: "600px", background: Cast(Doc.SharingDoc().userColor, "string", null) }} />; } }
\ No newline at end of file diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index b756716bf..9dc57dd1e 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -40,7 +40,7 @@ .permissions-select { z-index: 1; - margin-left: -100; + margin-left: -115; border: none; outline: none; text-align: justify; // for Edge @@ -59,6 +59,7 @@ margin-top: -17px; margin-bottom: 10px; font-size: 10px; + min-height: 40px; input { height: 10px; @@ -71,21 +72,25 @@ } } - .layoutDoc-acls { + .acl-container { display: flex; - flex-direction: column; float: right; - margin-right: 12; - margin-top: -15; - align-items: center; + align-items: baseline; + margin-top: -12; - label { - font-weight: normal; - font-style: italic; - } + .layoutDoc-acls, + .myDocs-acls { + flex-direction: column; + margin-right: 12; - input { - cursor: pointer; + label { + font-weight: normal; + font-style: italic; + } + + input { + cursor: pointer; + } } } } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index a53ef3cde..2b13d2a44 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -4,10 +4,10 @@ import { observer } from "mobx-react"; import * as React from "react"; import Select from "react-select"; import * as RequestPromise from "request-promise"; -import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym } from "../../fields/Doc"; +import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly, DocListCastAsync } from "../../fields/Doc"; import { List } from "../../fields/List"; import { Cast, StrCast } from "../../fields/Types"; -import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from "../../fields/util"; +import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx, normalizeEmail } from "../../fields/util"; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { CollectionView } from "../views/collections/CollectionView"; @@ -21,10 +21,12 @@ import { GroupMemberView } from "./GroupMemberView"; import "./SharingManager.scss"; import { SelectionManager } from "./SelectionManager"; import { intersection } from "lodash"; +import { SearchBox } from "../views/search/SearchBox"; export interface User { email: string; - userDocumentId: string; + sharingDocumentId: string; + linkDatabaseId: string; } /** @@ -46,20 +48,20 @@ const groupType = "!groupType/"; const storage = "data"; /** - * A user who also has a notificationDoc. + * A user who also has a sharing doc. */ interface ValidatedUser { - user: User; - notificationDoc: Doc; - userColor: string; + user: User; // database minimal info to identify / communicate with a user (email, sharing doc id) + sharingDoc: Doc; // document to share/message another user + linkDatabase: Doc; + userColor: string; // stored on the sharinDoc, extracted for convenience? } - @observer export class SharingManager extends React.Component<{}> { public static Instance: SharingManager; @observable private isOpen = false; // whether the SharingManager modal is open or not - @observable public users: ValidatedUser[] = []; // the list of users with notificationDocs + @observable public users: ValidatedUser[] = []; // the list of users with sharing docs @observable private targetDoc: Doc | undefined; // the document being shared @observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared // @observable private copied = false; @@ -70,18 +72,19 @@ export class SharingManager extends React.Component<{}> { @observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals @observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup + private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup // if both showUserOptions and showGroupOptions are false then both are displayed @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) private populating: boolean = false; // whether the list of users is populating or not @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used + @observable private myDocAcls: boolean = false; // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; // } public open = (target?: DocumentView, target_doc?: Doc) => { - runInAction(() => this.users = []); this.populateUsers(); runInAction(() => { this.targetDocView = target; @@ -116,30 +119,35 @@ export class SharingManager extends React.Component<{}> { } /** - * Populates the list of validated users (this.users) by adding registered users which have a mySharedDocs. + * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. */ populateUsers = async () => { if (!this.populating) { this.populating = true; - runInAction(() => this.users = []); const userList = await RequestPromise.get(Utils.prepend("/getUsers")); const raw = JSON.parse(userList) as User[]; + const sharingDocs: ValidatedUser[] = []; const evaluating = raw.map(async user => { const isCandidate = user.email !== Doc.CurrentUserEmail; if (isCandidate) { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument.mySharedDocs, Doc); - const userColor = StrCast(userDocument.userColor); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push({ user, notificationDoc, userColor }); - } - }); + const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId); + const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId); + if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { + await DocListCastAsync(linkDatabase.data); + sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.color) }); } } }); - return Promise.all(evaluating).then(() => this.populating = false); + return Promise.all(evaluating).then(() => { + runInAction(() => { + for (const sharer of sharingDocs) { + if (!this.users.find(user => user.user.email === sharer.user.email)) { + this.users.push(sharer); + } + } + }); + this.populating = false; + }); } } @@ -148,17 +156,17 @@ export class SharingManager extends React.Component<{}> { * @param group * @param permission */ - setInternalGroupSharing = (group: Doc | { groupName: string }, permission: string, targetDoc?: Doc) => { + setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { const target = targetDoc || this.targetDoc!; - const key = StrCast(group.groupName).replace(".", "_"); + const key = normalizeEmail(StrCast(group.title)); const acl = `acl-${key}`; const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); docs.forEach(doc => { - doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc); - GetEffectiveAcl(doc) === AclAdmin && distributeAcls(acl, permission as SharingPermissions, doc); + doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc); + distributeAcls(acl, permission as SharingPermissions, doc); if (group instanceof Doc) { const members: string[] = JSON.parse(StrCast(group.members)); @@ -167,9 +175,9 @@ export class SharingManager extends React.Component<{}> { // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]); - users.forEach(({ user, notificationDoc }) => { - if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc); // add the doc to the notificationDoc if it hasn't already been added - else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists + users.forEach(({ user, sharingDoc }) => { + if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists }); } }); @@ -180,16 +188,26 @@ export class SharingManager extends React.Component<{}> { * @param group * @param emailId */ - shareWithAddedMember = (group: Doc, emailId: string) => { - const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); + shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { + const user = this.users.find(({ user: { email } }) => email === emailId)!; + const self = this; + if (group.docsShared) { + if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); + else { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + })); + } + } } /** * Called from the properties sidebar to change permissions of a user. */ shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => { - if (shareWith !== "Public") { + if (shareWith !== "Public" && shareWith !== "Override") { const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith)); docs.forEach(doc => { if (user) this.setInternalSharing(user, permission, doc); @@ -198,7 +216,7 @@ export class SharingManager extends React.Component<{}> { } else { docs.forEach(doc => { - if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls("acl-Public", permission, doc); + if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc); }); } } @@ -212,9 +230,12 @@ export class SharingManager extends React.Component<{}> { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { - DocListCast(group.docsShared).forEach(doc => { - Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc); // remove the doc only if it is in the list - }); + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); } } @@ -225,14 +246,14 @@ export class SharingManager extends React.Component<{}> { removeGroup = (group: Doc) => { if (group.docsShared) { DocListCast(group.docsShared).forEach(doc => { - const acl = `acl-${StrCast(group.groupName)}`; + const acl = `acl-${StrCast(group.title)}`; distributeAcls(acl, SharingPermissions.None, doc); const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); + users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); }); } } @@ -241,20 +262,18 @@ export class SharingManager extends React.Component<{}> { * Shares the document with a user. */ setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { - const { user, notificationDoc } = recipient; + const { user, sharingDoc } = recipient; const target = targetDoc || this.targetDoc!; - const key = user.email.replace('.', '_'); - const acl = `acl-${key}`; - + const acl = `acl-${normalizeEmail(user.email)}`; + const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); - docs.forEach(doc => { - doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc); - GetEffectiveAcl(doc) === AclAdmin && distributeAcls(acl, permission as SharingPermissions, doc); + doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc); + distributeAcls(acl, permission as SharingPermissions, doc); - if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc); - else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (doc.aliasOf as Doc || doc)); + if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); }); } @@ -285,13 +304,14 @@ export class SharingManager extends React.Component<{}> { /** * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share */ - private sharingOptions(uniform: boolean) { + private sharingOptions(uniform: boolean, override?: boolean) { const dropdownValues: string[] = Object.values(SharingPermissions); if (!uniform) dropdownValues.unshift("-multiple-"); - return dropdownValues.map(permission => + if (override) dropdownValues.unshift("None"); + return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => ( <option key={permission} value={permission}> - {permission} + {permission === SharingPermissions.Add ? "Can Augment" : permission} </option> ) ); @@ -372,6 +392,25 @@ export class SharingManager extends React.Component<{}> { } } + distributeOverCollection = (targetDoc?: Doc) => { + const AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] + ]); + + const target = targetDoc || this.targetDoc!; + + const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); + docs.forEach(doc => { + for (const [key, value] of Object.entries(doc[AclSym])) { + distributeAcls(key, AclMap.get(value)! as SharingPermissions, target); + } + }); + } + /** * Sorting algorithm to sort users. */ @@ -385,8 +424,8 @@ export class SharingManager extends React.Component<{}> { * Sorting algorithm to sort groups. */ sortGroups = (group1: Doc, group2: Doc) => { - const g1 = StrCast(group1.groupName); - const g2 = StrCast(group2.groupName); + const g1 = StrCast(group1.title); + const g2 = StrCast(group2.title); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; } @@ -395,9 +434,9 @@ export class SharingManager extends React.Component<{}> { */ @computed get sharingInterface() { TraceMobx(); - const groupList = GroupManager.Instance?.getAllGroups() || []; + const groupList = GroupManager.Instance?.allGroups || []; const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email })); - const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); + const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) })); // the next block handles the users shown (individuals/groups/both) const options: GroupedOptions[] = []; @@ -415,21 +454,27 @@ export class SharingManager extends React.Component<{}> { const groups = this.groupSort === "ascending" ? groupList.slice().sort(this.sortGroups) : this.groupSort === "descending" ? groupList.slice().sort(this.sortGroups).reverse() : groupList; // handles the case where multiple documents are selected - const docs = SelectionManager.SelectedDocuments().length < 2 ? + let docs = SelectionManager.SelectedDocuments().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]] : SelectionManager.SelectedDocuments().map(docView => this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym]); + if (this.myDocAcls) { + const newDocs: Doc[] = []; + SearchBox.foreachRecursiveDoc(docs, doc => newDocs.push(doc)); + docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); + } + const targetDoc = docs[0]; // tslint:disable-next-line: no-unnecessary-callback-wrapper - const admin = docs.map(doc => GetEffectiveAcl(doc)).every(acl => acl === AclAdmin); // if the user has admin access to all selected docs + const admin = this.myDocAcls ? Boolean(docs.length) : docs.map(doc => GetEffectiveAcl(doc)).every(acl => acl === AclAdmin); // if the user has admin access to all selected docs // users in common between all docs const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))); // the list of users shared with - const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${user.email.replace('.', '_')}`) : true).map(({ user, notificationDoc, userColor }) => { - const userKey = `acl-${user.email.replace('.', '_')}`; + const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, linkDatabase, sharingDoc, userColor }) => { + const userKey = `acl-${normalizeEmail(user.email)}`; const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]); const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-"; @@ -440,17 +485,17 @@ export class SharingManager extends React.Component<{}> { > <span className={"padding"}>{user.email}</span> <div className="edit-actions"> - {admin ? ( + {admin || this.myDocAcls ? ( <select className={"permissions-dropdown"} value={permissions} - onChange={e => this.setInternalSharing({ user, notificationDoc, userColor }, e.currentTarget.value)} + onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)} > {this.sharingOptions(uniform)} </select> ) : ( <div className={"permissions-dropdown"}> - {permissions} + {permissions === SharingPermissions.Add ? "Can Augment" : permissions} </div> )} </div> @@ -486,7 +531,7 @@ export class SharingManager extends React.Component<{}> { <span className={"padding"}>Me</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> - {targetDoc?.[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`]} + {targetDoc?.[`acl-${Doc.CurrentUserEmailNormalized}`]} </div> </div> </div> @@ -495,32 +540,32 @@ export class SharingManager extends React.Component<{}> { // the list of groups shared with - const groupListMap: (Doc | { groupName: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${StrCast(groupName).replace('.', '_')}`) : true); - groupListMap.unshift({ groupName: "Public" }); + const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true); + groupListMap.unshift({ title: "Public" }, { title: "Override" }); const groupListContents = groupListMap.map(group => { - const groupKey = `acl-${StrCast(group.groupName)}`; + const groupKey = `acl-${StrCast(group.title)}`; const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]); - const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.groupName)}`]) : "-multiple-"; + const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : "-multiple-"; return !permissions ? (null) : ( <div key={groupKey} className={"container"} > - <div className={"padding"}>{group.groupName}</div> + <div className={"padding"}>{group.title}</div> {group instanceof Doc ? (<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}> <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> </div>) : (null)} <div className="edit-actions"> - {admin ? ( + {admin || this.myDocAcls ? ( <select className={"permissions-dropdown"} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} > - {this.sharingOptions(uniform)} + {this.sharingOptions(uniform, group.title === "Override")} </select> ) : ( <div className={"permissions-dropdown"}> @@ -572,8 +617,18 @@ export class SharingManager extends React.Component<{}> { <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label> <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label> </div> - <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> + + <div className="acl-container"> + <div className="myDocs-acls"> + <input type="checkbox" onChange={action(() => this.myDocAcls = !this.myDocAcls)} checked={this.myDocAcls} /> <label>My Docs</label> + </div> + {Doc.UserDoc().noviceMode ? (null) : + <div className="layoutDoc-acls"> + <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> + </div>} + <button className="distribute-button" onClick={() => this.distributeOverCollection()}> + Distribute + </button> </div> </div> } diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index fc07e8ab4..069f81d38 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,4 +1,5 @@ import { observable, action, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; export namespace SnappingManager { @@ -14,6 +15,9 @@ export namespace SnappingManager { this.horizSnapLines = horizLines; this.vertSnapLines = vertLines; } + + @observable cachedGroups: string[] = []; + @action setCachedGroups(groups: string[]) { this.cachedGroups = groups; } } const manager = new Manager(); @@ -25,5 +29,11 @@ export namespace SnappingManager { export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } export function GetIsDragging() { return manager.IsDragging; } + + /// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts + // need to investigate further what caused the mobx update problems and move to a better location. + const getCachedGroupByNameCache = computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true); + export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); } + export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); } } 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/DocComponent.tsx b/src/client/views/DocComponent.tsx index dad48d002..a55f4adaf 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; -import { GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../fields/util'; +import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail } from '../../fields/util'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -123,7 +123,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T @action.bound removeDocument(doc: Doc | Doc[]): boolean { const effectiveAcl = GetEffectiveAcl(this.dataDoc); - if (effectiveAcl === AclAdmin || effectiveAcl === AclEdit) { + const indocs = doc instanceof Doc ? [doc] : doc; + const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); + if (docs.length) { const docs = doc instanceof Doc ? [doc] : doc; docs.map(doc => doc.isPushpin = doc.annotationOn = undefined); const targetDataDoc = this.dataDoc; @@ -162,11 +164,12 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T return false; } else { - if (this.props.Document[AclSym]) { + if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) { added.forEach(d => { for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - if (d.author === key.substring(4).replace("_", ".") && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); - else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); + //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true); + // else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); } }); } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index c5fd3c777..fa0b9a238 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -25,6 +25,8 @@ import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; 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; @@ -185,33 +187,52 @@ 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={e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, false))}> + onClick={undoBatch(e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, false)))}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </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={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>; @@ -242,7 +263,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get menuButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`Open Context Menu`}</div></>}> - <div className="documentButtonBar-linker" style={{ color: "white", cursor: "context-menu" }} onClick={e => this.openContextMenu(e)}> + <div className="documentButtonBar-linker" style={{ color: "white", cursor: "pointer" }} onClick={e => this.openContextMenu(e)}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="bars" /> </div></Tooltip >; } @@ -282,12 +303,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - dragData.dropAction = "alias"; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { - offsetX: dragData.offset[0], - offsetY: dragData.offset[1], - hideSource: false - }); + dragData.defaultDropAction = "alias"; + dragData.canEmbed = true; + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); return true; } return false; @@ -303,7 +321,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV Array.from(["Caption", "Title", "TitleHover"]).map(template => templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template] ? true : false, false as boolean))); return !view0 ? (null) : - <Tooltip title={<div className="dash-tooltip">CustomizeLayout</div>} open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} placement="bottom"> + <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embeddable alias</div>} open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} placement="bottom"> <div className="documentButtonBar-linkFlyout" ref={this._dragRef} onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))} > @@ -363,9 +381,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV {![DocumentType.VID, DocumentType.WEB].includes(StrCast(this.view0.props.Document.type) as DocumentType) ? (null) : <div className="documentButtonBar-button"> {this.annotateButton} </div>} - <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> + {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} - </div> + </div>} <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}> {this.considerGoogleDocsPull} </div> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index dfcb6cda9..13eeaa3e7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -25,6 +25,7 @@ import React = require("react"); import e = require('express'); import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { InkStrokeProperties } from './InkStrokeProperties'; +import { KeyManager } from './GlobalKeyHandler'; @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -119,7 +120,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } @action onTitleDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onBackgroundMove, (e) => { }, this.onTitleClick); + setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, this.onTitleClick); } @action onTitleClick = (e: PointerEvent): void => { !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); @@ -128,22 +129,26 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } onBackgroundDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onBackgroundMove, (e) => { }, (e) => { }); + setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), (e) => { }, (e) => { }); } @action - onBackgroundMove = (e: PointerEvent, down: number[]): boolean => { + onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const dragDocView = SelectionManager.SelectedDocuments()[0]; const dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); dragData.moveDocument = dragDocView.props.moveDocument; dragData.isSelectionMove = true; + dragData.canEmbed = dragTitle; dragData.dropAction = dragDocView.props.dropAction; this.Interacting = true; this._hidden = true; DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { - dragComplete: action(e => this._hidden = this.Interacting = false), + dragComplete: action(e => { + dragData.canEmbed && SelectionManager.DeselectAll(); + this._hidden = this.Interacting = false; + }), hideSource: true }); return true; @@ -172,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"); } } } @@ -323,8 +326,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); } } })); @@ -352,11 +355,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); } })); @@ -390,7 +393,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": @@ -436,17 +439,17 @@ 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; const scale = docView.props.ScreenToLocalTransform().Scale * docView.props.ContentScaling(); if (nwidth && nheight) { - if (nwidth / nheight !== width / height) { + if (nwidth / nheight !== width / height && !dragBottom) { height = nheight / nwidth * width; } if (e.ctrlKey || (!dragBottom || !docView.layoutDoc._fitWidth)) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction @@ -459,26 +462,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; @@ -486,7 +486,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; @@ -572,6 +572,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> "caret-up"; return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } + render() { const darkScheme = CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimgray" : undefined; const bounds = this.Bounds; @@ -622,7 +623,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, - pointerEvents: this.Interacting ? "none" : "all", + pointerEvents: KeyManager.Instance.ShiftPressed || this.Interacting ? "none" : "all", zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0, }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} > </div> 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 d35271ffd..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, @@ -47,6 +48,7 @@ export interface EditableProps { onClick?: (e: React.MouseEvent) => boolean; isEditingCallback?: (isEditing: boolean) => void; menuCallback?: (x: number, y: number) => void; + textCallback?: (char: string) => boolean; showMenuOnLoad?: boolean; HeadingObject?: SchemaHeaderField | undefined; toggle?: () => void; @@ -81,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; @@ -119,19 +134,27 @@ export class EditableView extends React.Component<EditableProps> { case ":": this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; + case "Shift": case "Alt": case "Meta": case "Control": break; + default: + if (this.props.textCallback?.(e.key)) { + this._editing = false; + this.props.isEditingCallback?.(false,); + } } } @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 @@ -161,6 +184,7 @@ export class EditableView extends React.Component<EditableProps> { } _ref = React.createRef<HTMLDivElement>(); + _inputref = React.createRef<HTMLInputElement>(); renderEditor() { return this.props.autosuggestProps ? <Autosuggest @@ -178,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} @@ -204,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 97dfb7c50..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(), @@ -905,6 +906,7 @@ export class GestureOverlay extends Touchable { parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docRangeFilters={returnEmptyFilter} docFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 0c05a1b69..fb360ee26 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -1,4 +1,4 @@ -import { action } from "mobx"; +import { action, observable } from "mobx"; import { DateField } from "../../fields/DateField"; import { Doc, DocListCast } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; @@ -37,6 +37,7 @@ type KeyControlInfo = { export class KeyManager { public static Instance: KeyManager = new KeyManager(); private router = new Map<string, KeyHandler>(); + @observable ShiftPressed = false; constructor() { const isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0; @@ -49,7 +50,13 @@ export class KeyManager { this.router.set("1000", this.shift); } - public handle = async (e: KeyboardEvent) => { + public unhandle = action((e: KeyboardEvent) => { + if (e.key?.toLowerCase() === "shift") KeyManager.Instance.ShiftPressed = false; + }); + + 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.UPDATE_SERVER_CACHE(true); const keyname = e.key && e.key.toLowerCase(); this.handleGreedy(keyname); @@ -69,7 +76,7 @@ export class KeyManager { control.stopPropagation && e.stopPropagation(); control.preventDefault && e.preventDefault(); - } + }); private handleGreedy = action((keyname: string) => { switch (keyname) { @@ -128,7 +135,7 @@ export class KeyManager { }; }); - private shift = async (keyname: string) => { + private shift = action(async (keyname: string) => { const stopPropagation = false; const preventDefault = false; @@ -143,7 +150,7 @@ export class KeyManager { stopPropagation: stopPropagation, preventDefault: preventDefault }; - } + }); private alt = action((keyname: string) => { const stopPropagation = true; @@ -216,6 +223,11 @@ export class KeyManager { stopPropagation = false; break; case "a": + if (e.target !== document.body) { + stopPropagation = false; + preventDefault = false; + } + break; case "v": stopPropagation = false; preventDefault = false; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 2a7d4a5ad..2015e1155 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; } @@ -191,8 +191,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 77e37834d..c256d2ebb 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -10,17 +10,14 @@ import { CollectionView } from "./collections/CollectionView"; AssignAllExtensions(); -export let resolvedPorts: { server: number, socket: number }; - (async () => { window.location.search.includes("safe") && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); - resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts")); - DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email); - await Docs.Prototypes.initialize(); if (info.id !== "__guest__") { // a guest will not have an id registered - await CurrentUserUtils.loadUserDocument(info); + 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 f64449616..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; + } } } @@ -293,7 +333,6 @@ .mainView-libraryFlyout-out, .mainView-libraryFlyout { height: 100%; - width: 100%; position: relative; display: flex; flex-direction: column; @@ -319,6 +358,9 @@ .mainView-libraryFlyout-out { transition: width .25s; box-shadow: rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw; + .mainView-docButtons { + left: 0; + } } @@ -332,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 ad98f7ea2..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 @@ -86,7 +87,7 @@ export class MainView extends React.Component { document.getElementById("root")?.addEventListener("scroll", e => ((ele) => ele.scrollLeft = ele.scrollTop = 0)(document.getElementById("root")!)); new InkStrokeProperties(); this._sidebarContent.proto = undefined; - DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollY", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's + DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollY", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg))); @@ -96,6 +97,8 @@ export class MainView extends React.Component { firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); + window.removeEventListener("keyup", KeyManager.Instance.unhandle); + window.addEventListener("keyup", KeyManager.Instance.unhandle); window.addEventListener("paste", KeyManager.Instance.paste as any); document.addEventListener("dash", (e: any) => { // event used by chrome plugin to tell Dash which document to focus on const id = FormattedTextBox.GetDocFromUrl(e.detail); @@ -106,6 +109,7 @@ export class MainView extends React.Component { } componentWillUnMount() { + window.removeEventListener("keyup", KeyManager.Instance.unhandle); window.removeEventListener("keydown", KeyManager.Instance.handle); window.removeEventListener("pointerdown", this.globalPointerDown); window.removeEventListener("paste", KeyManager.Instance.paste as any); @@ -129,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, @@ -147,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(); } @@ -171,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) { @@ -224,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"; } } @@ -269,6 +278,7 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} @@ -308,38 +318,42 @@ export class MainView extends React.Component { } @computed get flyout() { - return <div className={`mainView-libraryFlyout${this._flyoutWidth ? "" : "-out"}`} style={{ minWidth: this._flyoutWidth, width: this._flyoutWidth }} > - <div className="mainView-contentArea" > - <DocumentView - Document={this._sidebarContent} - DataDoc={undefined} - LibraryPath={emptyPath} - addDocument={undefined} - addDocTab={this.addDocTabFunc} - pinToPres={emptyFunction} - rootSelected={returnTrue} - removeDocument={returnFalse} - onClick={undefined} - ScreenToLocalTransform={this.mainContainerXf} - ContentScaling={returnOne} - PanelWidth={this.flyoutWidthFunc} - PanelHeight={this.getContentsHeight} - renderDepth={0} - focus={emptyFunction} - backgroundColor={this.defaultBackgroundColors} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - relative={true} - forcedBackgroundColor={() => "lightgrey"} - /> - </div> + return !this._flyoutWidth ? <div className={`mainView-libraryFlyout-out`}> {this.docButtons} - </div>; + </div> : + <div className="mainView-libraryFlyout" style={{ minWidth: this._flyoutWidth, width: this._flyoutWidth }} > + <div className="mainView-contentArea" > + <DocumentView + Document={this._sidebarContent} + DataDoc={undefined} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={this.addDocTabFunc} + pinToPres={emptyFunction} + rootSelected={returnTrue} + removeDocument={returnFalse} + onClick={undefined} + ScreenToLocalTransform={this.mainContainerXf} + ContentScaling={returnOne} + PanelWidth={this.flyoutWidthFunc} + PanelHeight={this.getContentsHeight} + renderDepth={0} + focus={emptyFunction} + backgroundColor={this.defaultBackgroundColors} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + relative={true} + forcedBackgroundColor={() => this.darkScheme ? "rgb(62,62,62)" : "lightgrey"} + /> + </div> + {this.docButtons} + </div>; } @computed get menuPanel() { @@ -365,6 +379,7 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} @@ -388,6 +403,8 @@ export class MainView extends React.Component { SearchBox.Instance._searchFullDB = "My Stuff"; SearchBox.Instance.enter(undefined); break; + case "Help": + break; default: this.expandFlyout(button); } @@ -398,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> </>; } @@ -430,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; }); @@ -484,6 +501,7 @@ export class MainView extends React.Component { focus={emptyFunction} whenActiveChanged={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> @@ -549,6 +567,7 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> @@ -580,6 +599,7 @@ export class MainView extends React.Component { PanelHeight={() => 800} ContentScaling={returnOne} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div>; @@ -645,6 +665,7 @@ export class MainView extends React.Component { PanelHeight={() => 800} ContentScaling={returnOne} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div>; diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 4b8049e14..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[]) => { @@ -204,6 +199,7 @@ export class OverlayView extends React.Component { addDocTab={returnFalse} pinToPres={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 9f08a03e1..62e0fb379 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -58,6 +58,7 @@ export default class Palette extends React.Component<PaletteProps> { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 00754c416..9de4f9c67 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -397,7 +397,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { </div> </form> {Doc.UserDoc().noviceMode ? (null) : <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div>} - </div> + </div>; } @computed @@ -529,7 +529,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> {this.clustersButton} </div> - <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> + <div className="propertiesButtons-button" style={{ display: !isFreeForm && !isText ? "none" : "" }}> {this.fitContentButton} </div> <div className="propertiesButtons-button" style={{ display: !isInk ? "none" : "" }}> diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index e5f9e0417..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; @@ -121,19 +118,30 @@ padding: 10px; margin-left: 5px; - .propertiesView-acls-checkbox { + .propertiesView-buttonContainer { float: right; - height: 20px; - margin-top: -20px; - margin-right: -15; - - .propertiesView-acls-checkbox-text { - font-size: 7px; - margin-top: -10px; - margin-left: 6px; + display: flex; + + button { + width: 15; + height: 15; + padding: 0; + margin-top: -5; + } + + .propertiesView-acls-checkbox { + margin-top: -20px; + + .propertiesView-acls-checkbox-text { + font-size: 7px; + margin-top: -10px; + margin-left: 6px; + } } } + + .change-buttons { display: flex; @@ -246,15 +254,19 @@ } } - .expansion-button-icon { - width: 11px; - height: 11px; - color: black; - margin-left: 27px; + .expansion-button { + margin-left: -20; - &:hover { - color: rgb(131, 131, 131); - cursor: pointer; + .expansion-button-icon { + width: 11px; + height: 11px; + color: black; + margin-left: 27px; + + &:hover { + color: rgb(131, 131, 131); + cursor: pointer; + } } } @@ -305,9 +317,9 @@ .permissions-select { border: none; background-color: inherit; - width: 75px; - //text-align: justify; // for Edge - //text-align-last: end; + width: 87px; + text-align: justify; // for Edge + text-align-last: end; &:hover { cursor: pointer; @@ -320,7 +332,6 @@ } } } - .propertiesView-fields { //border-bottom: 1px solid black; //padding: 8.5px; @@ -375,7 +386,7 @@ } } - .field { + .propertiesView-field { display: flex; font-size: 7px; background-color: #e8e8e8; @@ -385,7 +396,7 @@ padding-left: 3px; } - .uneditable-field { + .propertiesView-uneditable-field { display: flex; overflow-y: visible; margin-bottom: 2px; @@ -671,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 c2c0d553b..fad2f5284 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -1,16 +1,17 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Checkbox, Tooltip } from "@material-ui/core"; +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, 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"; import { Cast, NumCast, StrCast } from "../../fields/Types"; -import { GetEffectiveAcl, SharingPermissions } from "../../fields/util"; -import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../Utils"; +import { GetEffectiveAcl, SharingPermissions, denormalizeEmail } from "../../fields/util"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne } from "../../Utils"; import { DocumentType } from "../documents/DocumentTypes"; import { DocumentManager } from "../util/DocumentManager"; import { SelectionManager } from "../util/SelectionManager"; @@ -26,7 +27,7 @@ import { PresBox } from "./nodes/PresBox"; import { PropertiesButtons } from "./PropertiesButtons"; import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; import "./PropertiesView.scss"; -import { intersection } from "lodash"; +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} @@ -284,6 +281,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { focus={returnFalse} ScreenToLocalTransform={this.getTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} @@ -320,13 +318,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { getPermissionsSelect(user: string, permission: string) { const dropdownValues: string[] = Object.values(SharingPermissions); if (permission === "-multiple-") dropdownValues.unshift(permission); + if (user === "Override") dropdownValues.unshift("None"); return <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> - {dropdownValues.map(permission => { + {dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => { return ( <option key={permission} value={permission}> - {permission} + {permission === SharingPermissions.Add ? "Can Augment" : permission} </option>); })} </select>; @@ -361,7 +360,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { /** * @returns a row of the permissions panel */ - sharingItem(name: string, admin: boolean, permission: string) { + sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { return <div className="propertiesView-sharingTable-item" key={name + permission} // style={{ backgroundColor: this.selectedUser === name ? "#bcecfc" : "" }} // onPointerDown={action(() => this.selectedUser = this.selectedUser === name ? "" : name)} @@ -370,7 +369,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {/* {name !== "Me" ? this.notifyIcon : null} */} <div className="propertiesView-sharingTable-item-permission"> {admin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission} - {permission === "Owner" ? this.expansionIcon : null} + {permission === "Owner" || showExpansionIcon ? this.expansionIcon : null} </div> </div>; } @@ -380,6 +379,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @computed get sharingTable() { const AclMap = new Map<symbol, string>([ + [AclUnset, "None"], [AclPrivate, SharingPermissions.None], [AclReadonly, SharingPermissions.View], [AclAddonly, SharingPermissions.Add], @@ -406,25 +406,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { if (commonKeys.length) { for (const key of commonKeys) { - const name = key.substring(4).replace("_", "."); + const name = denormalizeEmail(key.substring(4)); const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key]); - if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public"/* && sidebarUsersDisplayed![name] !== false*/) { + if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public" && name !== "Override"/* && sidebarUsersDisplayed![name] !== false*/) { tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : "-multiple-")); } } } - // if (Doc.UserDoc().sidebarUsersDisplayed) { - // for (const [name, value] of Object.entries(sidebarUsersDisplayed!)) { - // if (value === true && !this.selectedDoc![`acl-${name.substring(8).replace(".", "_")}`]) tableEntries.push(this.sharingItem(name.substring(8), effectiveAcl, SharingPermissions.None)); - // } - // } - // }) - + const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); // shifts the current user, owner, public to the top of the doc. + tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); tableEntries.unshift(this.sharingItem("Public", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Public"] === docs[0]["acl-Public"]) ? (AclMap.get(target[AclSym]?.["acl-Public"]) || SharingPermissions.None) : "-multiple-")); - tableEntries.unshift(this.sharingItem("Me", showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? "Owner" : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : "-multiple-")); - if (Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author)) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, "Owner")); + tableEntries.unshift(this.sharingItem("Me", showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? "Owner" : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : "-multiple-", !ownerSame)); + if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, "Owner")); return <div className="propertiesView-sharingTable"> {tableEntries} @@ -861,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 }}> @@ -896,14 +891,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {!this.openSharing ? (null) : <div className="propertiesView-sharing-content"> - {!novice ? (<div className="propertiesView-acls-checkbox"> - <Checkbox - color="primary" - onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} - checked={this.layoutDocAcls} - />; - <div className="propertiesView-acls-checkbox-text">Layout</div> - </div>) : (null)} + <div className="propertiesView-buttonContainer"> + {!novice ? (<div className="propertiesView-acls-checkbox"> + <Checkbox + color="primary" + onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} + checked={this.layoutDocAcls} + /> + <div className="propertiesView-acls-checkbox-text">Layout</div> + </div>) : (null)} + <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> + <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> + <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> + </button> + </Tooltip> + </div> {this.sharingTable} </div>} </div> @@ -981,8 +983,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </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 @@ -990,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> @@ -1009,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" : "" }}> @@ -1021,21 +1028,21 @@ 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 className="propertiesView-presTrails"> + </div>} */} + {/* <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })} style={{ backgroundColor: this.openAddSlide ? "black" : "" }}> @@ -1047,7 +1054,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.openAddSlide ? <div className="propertiesView-presTrails-content"> {PresBox.Instance.newDocumentDropdown} </div> : null} - </div> + </div> */} </div>; } } diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index cf2118cb2..94efff4ee 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -131,6 +131,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} rootSelected={returnFalse} onCheckedClick={this.scriptField} diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index 9269e6f33..093310755 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -83,23 +83,22 @@ export class Timeline extends React.Component<FieldViewProps> { } /////////lifecycle functions//////////// + @action componentDidMount() { const relativeHeight = window.innerHeight / 20; //sets height to arbitrary size, relative to innerHeight this._titleHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //check if relHeight is less than Maxheight. Else, just set relheight to max this.MIN_CONTAINER_HEIGHT = this._titleHeight + 130; //offset this.DEFAULT_CONTAINER_HEIGHT = this._titleHeight * 2 + 130; //twice the titleheight + offset - runInAction(() => { - if (!this.props.Document.AnimationLength) { //if animation length did not exist - this.props.Document.AnimationLength = this._time; //set it to default time - } else { - this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before - } - this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself) - this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see) - this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts - this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on - this.toggleHandle(); - }); + if (!this.props.Document.AnimationLength) { //if animation length did not exist + this.props.Document.AnimationLength = this._time; //set it to default time + } else { + this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before + } + this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself) + this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see) + this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts + this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on + this.toggleHandle(); } componentWillUnmount() { @@ -485,7 +484,6 @@ export class Timeline extends React.Component<FieldViewProps> { render() { setTimeout(() => this.changeLengths(), 0); - trace(); // change visible and total width return ( <div style={{ visibility: "visible" }}> diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 4f1ef6e61..4e30709a6 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -166,7 +166,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume render() { const index = NumCast(this.layoutDoc._itemIndex); - const translateX = (1 - index) / this.childLayoutPairs.length * 100; + const translateX = 33 * (1 - index); return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> <div className="carousel-wrapper" style={{ transform: `translateX(calc(${translateX}%` }}> 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 4685d2ffc..abe8477e4 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -21,6 +21,9 @@ import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; 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 @@ -107,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; @@ -139,19 +143,35 @@ 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); if (!pullSide && stack) { stack.addChild(docContentConfig, undefined); + stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]); } else { const newItemStackConfig = { type: 'stack', content: [docContentConfig] }; const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isStack) { + instance._goldenLayout.root.contentItems[0].addChild(docContentConfig); } else if (instance._goldenLayout.root.contentItems.length === 1 && instance._goldenLayout.root.contentItems[0].contentItems.length === 1 && instance._goldenLayout.root.contentItems[0].contentItems[0].contentItems.length === 0) { instance._goldenLayout.root.contentItems[0].contentItems[0].addChild(docContentConfig); @@ -304,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 ae9915331..b6ab3f0e0 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -38,8 +38,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { componentDidMount() { // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). - this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.linearViewIsExpanded ? 1 : 0), - () => this.props.Document._width = 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + this._widthDisposer = reaction(() => 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + width => this.childDocs.length && (this.props.Document._width = width), { fireImmediately: true } ); @@ -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={{ @@ -160,6 +159,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> 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 4c5a71ace..b2b23115f 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -198,7 +198,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp self.target._searchFilterDocs = compareLists(self['target-searchFilterDocs'],self.target._searchFilterDocs) ? undefined: copyField(self['target-searchFilterDocs']);`, immediate: undoBatch((source: Doc[]) => { this.target._docFilters = undefined; this.target._searchFilterDocs = undefined; }), initialize: (button: Doc) => { - button['target-docFilters'] = Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters instanceof ObjectField ? ObjectField.MakeCopy(Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters as any as ObjectField) : undefined; + button['target-docFilters'] = (Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters || Cast(Doc.UserDoc().activeDashboard, Doc, null)._docFilters) instanceof ObjectField ? + ObjectField.MakeCopy((Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters || Cast(Doc.UserDoc().activeDashboard, Doc, null)._docFilters) as any as ObjectField) : undefined; button['target-searchFilterDocs'] = CurrentUserUtils.ActiveDashboard._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(CurrentUserUtils.ActiveDashboard._searchFilterDocs as any as ObjectField) : undefined; }, }; @@ -233,8 +234,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @undoBatch viewChanged = (e: React.ChangeEvent) => { + const target = this.document !== Doc.UserDoc().sidebar ? this.document : this.document.proto as Doc; //@ts-ignore - this.document._viewType = e.target.selectedOptions[0].value; + target._viewType = e.target.selectedOptions[0].value; } commandChanged = (e: React.ChangeEvent) => { @@ -342,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"}> @@ -365,7 +368,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </select> </div> </Tooltip> - </div>; + </div>); } @computed get selectedDocumentView() { @@ -392,28 +395,49 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </Tooltip>; } + @undoBatch + @action + pinWithView = (targetDoc: Opt<Doc>) => { + if (targetDoc) { + TabDocView.PinDoc(targetDoc, false); + 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 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; + activeDoc.presPinView = true; + 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; + } + } + } + @computed get pinWithViewButton() { 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)) ? (null) : <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={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; - } - }}> + onClick={() => this.pinWithView(targetDoc)}> {presPinWithViewIcon} </button> - </Tooltip>; + </Tooltip> : (null); } @@ -430,22 +454,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp this.selectedDocumentView.props.addDocument?.(alias); } } - private _dragRef = React.createRef<HTMLButtonElement>(); - - @observable _aliasDown = false; onAliasButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); } @undoBatch onAliasButtonMoved = (e: PointerEvent) => { - if (this._dragRef.current && this.selectedDoc) { - const dragData = new DragManager.DocumentDragData([this.selectedDoc]); - const [left, top] = [e.clientX, e.clientY]; + const contentDiv = this.selectedDocumentView?.ContentDiv; + if (contentDiv) { + const dragData = new DragManager.DocumentDragData([this.selectedDocumentView!.props.Document]); + const offset = [e.clientX - contentDiv.getBoundingClientRect().x, e.clientY - contentDiv.getBoundingClientRect().y]; dragData.defaultDropAction = "alias"; - DragManager.StartDocumentDrag([this._dragRef.current], dragData, left, top, { - offsetX: dragData.offset[0], - offsetY: dragData.offset[1], + dragData.canEmbed = true; + DragManager.StartDocumentDrag([contentDiv], dragData, e.clientX, e.clientY, { + offsetX: offset[0], + offsetY: offset[1], hideSource: false }); return true; @@ -456,8 +479,8 @@ 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"> - <button className="antimodeMenu-button" ref={this._dragRef} onPointerDown={this.onAliasButtonDown} onClick={this.onAlias} style={{ cursor: "copy" }}> + 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> </Tooltip>; @@ -466,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" ref={this._dragRef} 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 ( @@ -490,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} @@ -517,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); @@ -528,12 +558,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get isText() { - if (this.selectedDoc) { - const layoutField = Doc.LayoutField(this.selectedDoc); - const layoutStr = this.selectedDocumentView?.props.LayoutTemplateString || StrCast(layoutField); - return (document.activeElement as any)?.className.includes("ProseMirror") || layoutStr.includes("FormattedText") || StrCast((layoutField as Doc)?.layout).includes("FormattedText"); - } - else return false; + return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; } @undoBatch @@ -750,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; @@ -842,22 +868,26 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu render() { return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> - {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> - <div className="backKeyframe" onClick={this.prevKeyframe}> - <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> - </div> - </Tooltip> : null} - {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> - <div className="numKeyframe" style={{ color: this.document.editing ? "white" : "black", backgroundColor: this.document.editing ? "#5B9FDD" : "#AEDDF8" }} - onClick={action(() => this.document.editing = !this.document.editing)} > - {NumCast(this.document._currentFrame)} - </div> - </Tooltip> : null} - {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> - <div className="fwdKeyframe" onClick={this.nextKeyframe}> - <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> - </div> - </Tooltip> : null} + {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? + <> + <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> + <div className="backKeyframe" onClick={this.prevKeyframe}> + <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> + </div> + </Tooltip> + <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> + <div className="numKeyframe" style={{ color: this.document.editing ? "white" : "black", backgroundColor: this.document.editing ? "#5B9FDD" : "#AEDDF8" }} + onClick={action(() => this.document.editing = !this.document.editing)} > + {NumCast(this.document._currentFrame)} + </div> + </Tooltip> + <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> + <div className="fwdKeyframe" onClick={this.nextKeyframe}> + <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> + </div> + </Tooltip> + </> + : null} {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText || this.props.isDoc ? (null) : this.urlEditor @@ -871,7 +901,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </> : (null) } - {this.isText ? <RichTextMenu /> : null} + {<div style={{ display: !this.isText ? "none" : undefined }}><RichTextMenu /></div>} </div>; } } @@ -893,14 +923,14 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu if (docs instanceof Doc) { const keys = Object.keys(docs).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || - (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_")); + (key[0].toUpperCase() === key[0] && key[0] !== "_")); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } else { const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || - (key[0]?.toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_")); + (key[0]?.toUpperCase() === key[0] && key[0] !== "_")); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } } @@ -1301,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 2443bd34e..0b3dfe1e4 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -14,7 +14,7 @@ import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../fields/ScriptField"; import { BoolCast, Cast, DateCast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; -import { Utils } from "../../../Utils"; +import { Utils, emptyFunction } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; @@ -167,7 +167,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { if (this.props.Document._searchDoc) { const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); - DocumentManager.Instance.jumpToDocument(this._rowDoc, false, () => undefined, targetContext); + DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext, + undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); } } renderCellWithType(type: string | undefined) { @@ -240,8 +241,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const script = value.substring(value.startsWith("=:=") ? 3 : 2); retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col); } else { - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - script.compiled && (retVal = this.applyToDoc(this._rowDataDoc, this.props.row, this.props.col, script.run)); + const inputscript = value.substring(value.startsWith("=") ? 1 : 0); + const script = CompileScript(inputscript, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + script.compiled && (retVal = this.applyToDoc(inputscript.length !== value.length ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } if (retVal) { this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing' @@ -396,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/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index c02e88829..b408fd680 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -261,7 +261,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { componentDidMount() { document.addEventListener("pointerdown", this.detectClick); const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters?.includes(this._key)) { + if (filters?.some(filter => filter.split(":")[0] === this._key)) { runInAction(() => this.closeResultsVisibility = "contents"); } } @@ -297,11 +297,8 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { else { Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); this.updateFilter(); - let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const blockedkeys = ["system", "title-custom", "limitHeight", "proto", "x", "y", "zIndex", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n) && !n.startsWith("_") && !n.startsWith("acl")); - if (keyOptions.length) { - this.onSelect(keyOptions[0]); + if (this.showKeys.length) { + this.onSelect(this.showKeys[0]); } else if (this._searchTerm !== "" && this.props.canAddNew) { this.setSearchTerm(this._searchTerm || this._key); this.onSelect(this._searchTerm); @@ -320,22 +317,22 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.setIsEditing(true); } + @computed get showKeys() { + const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"]; + const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); + const showKeys = new Set<string>(); + [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.UserDoc().noviceMode || + whitelistKeys.includes(key) + || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null); + return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm)); + } @action renderOptions = (): JSX.Element[] | JSX.Element => { if (!this._isOpen) { this.defaultMenuHeight = 0; return <></>; } - const searchTerm = this._searchTerm.trim() === "New field" ? "" : this._searchTerm; - - let keyOptions = searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || - this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; - - const blockedkeys = ["proto", "x", "y", "zIndex", "_timeStampOnEnter", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n) && !n.startsWith("_") && !n.startsWith("acl")); - - const options = keyOptions.map(key => { + const options = this.showKeys.map(key => { return <div key={key} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", @@ -352,7 +349,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { // if search term does not already exist as a group type, give option to create new group type if (this._key !== this._searchTerm.slice(0, this._key.length)) { - if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + if (this._searchTerm !== "" && this.props.canAddNew) { options.push(<div key={""} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", }} @@ -399,19 +396,21 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { }); const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) { this.props.col.setColor("rgb(241, 239, 235)"); this.closeResultsVisibility = "none"; } - for (let i = 0; i < (filters?.length ?? 0) - 1; i += 3) { - if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i + 1]) === false) { + for (let i = 0; i < (filters?.length ?? 0) - 1; i++) { + if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i].split(":")[1]) === false) { keyOptions.push(filters![i + 1]); } } const options = keyOptions.map(key => { let bool = false; if (filters !== undefined) { - bool = filters.includes(key) && filters[filters.indexOf(key) + 1] === "check"; + const ind = filters.findIndex(filter => filter.split(":")[0] === key); + const fields = ind === -1 ? undefined : filters[ind].split(":"); + bool = fields ? fields[1] === "check" : false; } return <div key={key} className="key-option" style={{ border: "1px solid lightgray", paddingLeft: 5, textAlign: "left", @@ -456,7 +455,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { updateFilter() { const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) { this.props.col.setColor("rgb(241, 239, 235)"); this.closeResultsVisibility = "none"; } diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 8d2f645d9..2bdd280ec 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -30,7 +30,7 @@ .collectionSchemaView-dividerDragger { position: relative; height: 100%; - width: 20px; + width: $SCHEMA_DIVIDER_WIDTH; z-index: 20; right: 0; top: 0; @@ -228,6 +228,9 @@ position: absolute; background: white; padding: 5px; + position: fixed; + background: white; + border: black 1px solid; .collectionSchema-header-toggler { z-index: 100; @@ -500,7 +503,7 @@ button.add-column { border: grey; border-style: solid; border-width: 1px; - height: 100%; + height: 30px; .collectionSchemaView-dropdownButton { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 332a2001f..9f8468253 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -11,12 +11,12 @@ import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { Cast, NumCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents } from "../../../Utils"; +import { emptyFunction, returnFalse, returnOne, setupMoveUpEvents } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; -import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import '../DocumentDecorations.scss'; @@ -47,24 +47,26 @@ const columnTypes: Map<string, ColumnType> = new Map([ @observer export class CollectionSchemaView extends CollectionSubView(doc => doc) { private _previewCont?: HTMLDivElement; - private DIVIDER_WIDTH = 4; - - @observable previewDoc: Doc | undefined = undefined; - @observable private _focusedTable: Doc = this.props.Document; - - @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } - @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } - @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } - @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } + @observable _previewDoc: Doc | undefined = undefined; + @observable _focusedTable: Doc = this.props.Document; + @observable _col: any = ""; @observable _menuWidth = 0; @observable _headerOpen = false; @observable _headerIsEditing = false; - @observable _col: any = ""; @observable _menuHeight = 0; @observable _pointerX = 0; @observable _pointerY = 0; @observable _openTypes: boolean = false; + + @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } + @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } + @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth(); } + @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } + @computed get scale() { return this.props.ScreenToLocalTransform().Scale; } + @computed get columns() { return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); } + set columns(columns: SchemaHeaderField[]) { this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns); } + @computed get menuCoordinates() { let searchx = 0; let searchy = 0; @@ -81,15 +83,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return this.props.ScreenToLocalTransform().transformPoint(x, y); } - @computed get scale() { return this.props.ScreenToLocalTransform().Scale; } - - @computed get columns() { - return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); - } - set columns(columns: SchemaHeaderField[]) { - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns); - } - get documentKeys() { const docs = this.childDocs; const keys: { [key: string]: boolean } = {}; @@ -104,27 +97,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.columns.forEach(key => keys[key.heading] = true); return Array.from(Object.keys(keys)); } - @computed get possibleKeys() { return this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); } @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing; - - @action - changeColumnType = (type: ColumnType, col: any): void => { - this._openTypes = false; - this.setColumnType(col, type); - } - - changeColumnSort = (desc: boolean | undefined, col: any): void => { - this.setColumnSort(col, desc); - } - - changeColumnColor = (color: string, col: any): void => { - this.setColumnColor(col, color); - } - @undoBatch setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => { + this._openTypes = false; if (columnTypes.get(columnField.heading)) return; const columns = this.columns; @@ -165,42 +143,42 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { const type = col.type; - const anyType = <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any, col)}> + const anyType = <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Any)}> <FontAwesomeIcon icon={"align-justify"} size="sm" /> Any </div>; - const numType = <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number, col)}> + const numType = <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Number)}> <FontAwesomeIcon icon={"hashtag"} size="sm" /> Number </div>; - const textType = <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String, col)}> + const textType = <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.String)}> <FontAwesomeIcon icon={"font"} size="sm" /> Text </div>; - const boolType = <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean, col)}> + const boolType = <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Boolean)}> <FontAwesomeIcon icon={"check-square"} size="sm" /> Checkbox </div>; - const listType = <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List, col)}> + const listType = <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.List)}> <FontAwesomeIcon icon={"list-ul"} size="sm" /> List </div>; - const docType = <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc, col)}> + const docType = <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Doc)}> <FontAwesomeIcon icon={"file"} size="sm" /> Document </div>; - const imageType = <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image, col)}> + const imageType = <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Image)}> <FontAwesomeIcon icon={"image"} size="sm" /> Image </div>; - const dateType = <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date, col)}> + const dateType = <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Date)}> <FontAwesomeIcon icon={"calendar"} size="sm" /> Date </div>; @@ -239,15 +217,15 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchema-headerMenu-group"> <label>Sort by:</label> <div className="columnMenu-sort"> - <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true, col)}> + <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.setColumnSort(col, true)}> <FontAwesomeIcon icon="sort-amount-down" size="sm" /> Sort descending </div> - <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false, col)}> + <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.setColumnSort(col, false)}> <FontAwesomeIcon icon="sort-amount-up" size="sm" /> Sort ascending </div> - <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined, col)}> + <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}> <FontAwesomeIcon icon="times" size="sm" /> Clear sorting </div> @@ -270,12 +248,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchema-headerMenu-group"> <label>Color:</label> <div className="columnMenu-colors"> - <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!, col)}></div> - <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!, col)}></div> - <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!, col)}></div> - <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!, col)}></div> - <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!, col)}></div> - <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray, col)}></div> + <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div> + <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div> + <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div> + <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div> + <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div> + <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div> </div> </div> ); @@ -320,8 +298,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action closeHeader = () => { this._headerOpen = false; } - - @undoBatch @action deleteColumn = (key: string) => { @@ -351,15 +327,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onWheel(e: React.WheelEvent) { const scale = this.props.ScreenToLocalTransform().Scale; this.props.active(true) && e.stopPropagation(); - //this.menuCoordinates[0] -= e.screenX / scale; - //this.menuCoordinates[1] -= e.screenY / scale; } @computed get renderMenuContent() { TraceMobx(); return <div className="collectionSchema-header-menuOptions"> {this.renderTypes(this._col)} - {/* {this.renderSorting(this._col)} */} {this.renderColors(this._col)} <div className="collectionSchema-headerMenu-group"> <button onClick={() => { this.deleteColumn(this._col.heading); }} @@ -379,7 +352,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setPreviewDoc = (doc: Opt<Doc>) => { SelectionManager.SelectSchemaDoc(this, doc); - this.previewDoc = doc; + this._previewDoc = doc; } //toggles preview side-panel of schema @@ -389,7 +362,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } onDividerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, action(() => this.toggleExpander())); + setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander); } @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { @@ -405,21 +378,19 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { if (this.props.isSelected(true)) e.stopPropagation(); - else { - this.props.select(false); - } + else this.props.select(false); } } @computed - get previewDocument(): Doc | undefined { return this.previewDoc; } + get previewDocument(): Doc | undefined { return this._previewDoc; } @computed get dividerDragger() { return this.previewWidth() === 0 ? (null) : - <div className="collectionSchemaView-dividerDragger" - onPointerDown={this.onDividerDown} - style={{ width: `${this.DIVIDER_WIDTH}px` }} />; + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} > + <div className="collectionSchemaView-dividerDragger" /> + </div>; } @computed @@ -439,6 +410,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { PanelHeight={this.previewHeight} ScreenToLocalTransform={this.getPreviewTransform} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} @@ -484,8 +456,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { documentKeys={this.documentKeys} headerIsEditing={this._headerIsEditing} openHeader={this.openHeader} - onClick={e => { e.stopPropagation(); this.closeHeader(); }} - onPointerDown={this.onTablePointerDown} + onClick={this.onTableClick} + onPointerDown={emptyFunction} onResizedChange={this.onResizedChange} setColumns={this.setColumns} reorderColumns={this.reorderColumns} @@ -500,18 +472,19 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return <div className="collectionSchemaView-toolbar"> <div className="collectionSchemaView-toolbar-item"> <div id="preview-schema-checkbox-div"> - <input type="checkbox" - key={"Show Preview"} checked={this.previewWidth() !== 0} - onChange={this.toggleExpander} />Show Preview</div> + <input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> + Show Preview + </div> </div> </div>; } + onSpecificMenu = (e: React.MouseEvent) => { if ((e.target as any)?.className?.includes?.("collectionSchemaView-cell") || (e.target instanceof HTMLSpanElement)) { const cm = ContextMenu.Instance; const options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - optionItems.push({ description: "remove", event: () => this.previewDoc && this.props.removeDocument(this.previewDoc), icon: "trash" }); + optionItems.push({ description: "remove", event: () => this._previewDoc && this.props.removeDocument(this._previewDoc), icon: "trash" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); cm.displayMenu(e.clientX, e.clientY); (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. @@ -520,15 +493,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @action - onTablePointerDown = (e: React.PointerEvent): void => { + onTableClick = (e: React.MouseEvent): void => { if (!(e.target as any)?.className?.includes?.("collectionSchemaView-cell") && !(e.target instanceof HTMLSpanElement)) { this.setPreviewDoc(undefined); - } - this.setFocused(this.props.Document); - if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { + } else { e.stopPropagation(); } - // this.closeHeader(); + this.setFocused(this.props.Document); + this.closeHeader(); } onResizedChange = (newResized: Resize[], event: any) => { @@ -558,39 +530,16 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.columns = columns; } - onZoomMenu = (e: React.WheelEvent) => { - this.props.active(true) && e.stopPropagation(); - if (this.menuCoordinates[0] > e.screenX) { - this.menuCoordinates[0] -= e.screenX; //* this.scale; - } else { - this.menuCoordinates[0] += e.screenX; //* this.scale; - } - if (this.menuCoordinates[1] > e.screenY) { - this.menuCoordinates[1] -= e.screenY; //* this.scale; - } else { - this.menuCoordinates[1] += e.screenY; //* this.scale; - } - } - - + onZoomMenu = (e: React.WheelEvent) => this.props.active(true) && e.stopPropagation(); - onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { - } render() { - let name = "collectionSchemaView-container"; - if (this.props.Document._searchDoc) { - name = "collectionSchemaView-searchContainer"; - } - if (!this.props.active()) setTimeout(() => this.closeHeader(), 0); TraceMobx(); + if (!this.props.active()) setTimeout(() => this.closeHeader(), 0); const menuContent = this.renderMenuContent; const menu = <div className="collectionSchema-header-menu" onWheel={e => this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} - style={{ - position: "fixed", background: "white", border: "black 1px solid", - transform: `translate(${(this.menuCoordinates[0])}px, ${(this.menuCoordinates[1])}px)` - }}> + style={{ transform: `translate(${(this.menuCoordinates[0])}px, ${(this.menuCoordinates[1])}px)` }}> <Measure offset onResize={action((r: any) => { const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); this._menuWidth = dim[0]; this._menuHeight = dim[1]; @@ -598,7 +547,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>} </Measure> </div>; - return <div className={name} + return <div className={"collectionSchemaView" + (this.props.Document._searchDoc ? "-searchContainer" : "-container")} style={{ overflow: this.props.overflow === true ? "scroll" : undefined, backgroundColor: "white", pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, @@ -606,7 +555,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { }} > <div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} - onKeyPress={this.onKeyPress} onContextMenu={this.onSpecificMenu} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 8b2a30b12..97eacaeab 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -28,13 +28,18 @@ 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]>; const StackingDocument = makeInterface(collectionSchema, documentSchema); +export type collectionStackingViewProps = { + chromeStatus?: string; +}; + @observer -export class CollectionStackingView extends CollectionSubView(StackingDocument) { +export class CollectionStackingView extends CollectionSubView<StackingDocument, Partial<collectionStackingViewProps>>(StackingDocument) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); _pivotFieldDisposer?: IReactionDisposer; @@ -44,17 +49,18 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) @observable _heightMap = new Map<string, number>(); @observable _cursor: CursorProperty = "grab"; @observable _scroll = 0; // used to force the document decoration to update when scrolling + @computed get chromeStatus() { return this.props.chromeStatus || StrCast(this.layoutDoc._chromeStatus); } @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField)); } @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } + @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, this.props.yMargin || NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.layoutDoc._columnsStack, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.pivotField && (this.layoutDoc._chromeStatus !== 'view-mode' && this.layoutDoc._chromeStatus !== 'disabled')); } + @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; } @@ -165,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]); @@ -176,9 +182,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); smoothScroll(doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } - afterFocus && setTimeout(() => { - if (afterFocus?.()) { } - }, 500); + afterFocus && setTimeout(afterFocus, 500); } getDisplayDoc(doc: Doc, dxf: () => Transform, width: () => number) { @@ -209,6 +213,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) opacity={opacity} focus={this.focusDocument} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} @@ -229,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) => { @@ -273,38 +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; - if (docs) { - newDocs.map((doc, i) => { - console.log(doc.title); - if (i === 0) { - if (targInd === -1) targInd = docs.length; - else targInd = docs.indexOf(this.filteredChildren[targInd]); - const srcInd = docs.indexOf(doc); - docs.splice(srcInd, 1); - docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc); - } else if (i < (newDocs.length / 2)) { //glr: for some reason dragged documents are duplicated - if (targInd === -1) targInd = docs.length; - else targInd = docs.indexOf(newDocs[0]) + 1; - const srcInd = docs.indexOf(doc); - docs.splice(srcInd, 1); - docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc); - } - }); + DragManager.docsBeingDragged = []; + 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); } } } @@ -355,7 +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()) { - Doc.Layout(doc)._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); + 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); @@ -407,7 +409,8 @@ 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()) { - Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + 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"]), NumCast(doc[this.props.fieldKey + "-height"])); } })); this.observer.observe(ref); @@ -445,10 +448,6 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; } - onToggle = (checked: Boolean) => { - this.layoutDoc._chromeStatus = checked ? "collapsed" : "view-mode"; - } - onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped()) { @@ -471,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; } @@ -495,10 +494,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) width: `${1 / this.scaling * 100}%`, transformOrigin: "top left", }} - onScroll={action(e => { - if (!this.props.isSelected(true) && this.props.renderDepth) e.currentTarget.scrollTop = this._scroll; - else this._scroll = e.currentTarget.scrollTop; - })} + onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={e => this.props.active(true) && e.stopPropagation()} > @@ -508,13 +504,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.layoutDoc._chromeStatus !== 'disabled' && this.props.isSelected() ? <Switch - onChange={this.onToggle} - onClick={this.onToggle} - defaultChecked={this.layoutDoc._chromeStatus !== 'view-mode'} - checkedChildren="edit" - unCheckedChildren="view" - /> : null} + {/* {this.chromeStatus === 'disabled' || !this.props.isSelected() ? (null) : + <Switch + onChange={this.onToggle} + onClick={this.onToggle} + defaultChecked={this.chromeStatus !== 'view-mode'} + checkedChildren="edit" + unCheckedChildren="view" + />} */} </div> </div> ); } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 44d264b36..b7562c45e 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -22,6 +22,8 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; +import { Id } from "../../../fields/FieldSymbols"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -129,16 +131,22 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } @action - addDocument = (value: string, shiftDown?: boolean) => { - if (!value) return false; + textCallback = (char: string) => { + return this.addDocument("", false, true); + } + + @action + addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { + if (!value && !forceEmptyNote) return false; const key = StrCast(this.props.parent.props.Document._pivotField); const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; - this.props.parent.props.addDocument(newDoc); - return false; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? "" : " "; + return this.props.parent.props.addDocument(newDoc); } @action @@ -228,7 +236,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const docItems: ContextMenuProps[] = []; const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document; - DocUtils.addDocumentCreatorMenuItems(this.props.parent.props.addDocument, this.props.parent.props.addDocument, x, y); + DocUtils.addDocumentCreatorMenuItems((doc) => { + FormattedTextBox.SelectOnLoad = doc[Id]; + return this.props.parent.props.addDocument(doc); + }, this.props.parent.props.addDocument, x, y, true); Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => docItems.push({ @@ -256,13 +267,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } }, icon: "compress-arrows-alt" })); - layoutItems.push({ description: ":freeform", event: () => this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200 })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":columns", event: () => this.props.parent.props.addDocument(Docs.Create.MulticolumnDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":image", event: () => this.props.parent.props.addDocument(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); - - ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); - ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); ContextMenu.Instance.setDefaultItem("::", (name: string): void => { Doc.GetProto(this.props.parent.props.Document)[name] = ""; const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); @@ -285,7 +291,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const style = this.props.parent; const singleColumn = style.isStackingView; - const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin, 5); + const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.yMargin, 5); const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); const evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { @@ -299,6 +305,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const newEditableViewProps = { GetValue: () => "", SetValue: this.addDocument, + textCallback: this.textCallback, contents: "+ NEW", HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, @@ -306,10 +313,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ - marginTop: NumCast(this.props.parent.props.Document._yMargin, 5), + marginTop: NumCast(this.props.parent.yMargin, 5), width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.chromeStatus !== 'view-mode' && this.props.parent.chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. @@ -342,7 +349,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; - const chromeStatus = this.props.parent.props.Document._chromeStatus; + const chromeStatus = this.props.parent.chromeStatus; const type = this.props.parent.props.Document.type; return <> {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} @@ -365,7 +372,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled' && type !== DocumentType.PRES) ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 70 }}> + style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 10 }}> <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> </div> : null} </div> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index d9c4d274a..b27f64ff0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,6 +1,6 @@ import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; import CursorField from "../../../fields/CursorField"; -import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; +import { Doc, Opt, Field, DocListCast, AclPrivate } from "../../../fields/Doc"; import { Id, ToString } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; @@ -101,7 +101,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: get childLayoutPairs(): { layout: Doc; data: Doc; }[] { const { Document, DataDoc } = this.props; - const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, doc)).filter(pair => pair.layout); + const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, doc)). + filter(pair => { // filter out any documents that have a proto that we don't have permissions to (which we determine by not having any keys + return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length)); + }); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } get childDocList() { @@ -111,6 +114,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: return this.props.ignoreFields?.includes("_docFilters") ? [] : [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } + docRangeFilters = () => { + return this.props.ignoreFields?.includes("_docRangeFilters") ? [] : + [...this.props.docRangeFilters(), ...Cast(this.props.Document._docRangeFilters, listSpec("string"), [])]; + } searchFilterDocs = () => { return [...this.props.searchFilterDocs(), ...DocListCast(this.props.Document._searchFilterDocs)]; } @@ -127,18 +134,19 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : []; } - const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc); + const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate).map(d => d as Doc); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; const docFilters = this.docFilters(); + const docRangeFilters = this.docRangeFilters(); const searchDocs = this.searchFilterDocs(); - if (this.props.Document.dontRegisterView || (!docFilters.length && !searchDocs.length)) return childDocs; + if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) return childDocs; const docsforFilter: Doc[] = []; - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); childDocs.forEach((d) => { - let notFiltered = d.z || ((!searchDocs.length || searchDocs.includes(d)) && (!docFilters.length || DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript).length > 0)); + if (DocUtils.Excluded(d, docFilters)) return; + let notFiltered = d.z || ((!searchDocs.length || searchDocs.includes(d)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript).length > 0)); const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); const data = d[annos ? fieldKey + "-annotations" : fieldKey]; @@ -146,13 +154,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: let subDocs = DocListCast(data); if (subDocs.length > 0) { let newarray: Doc[] = []; - notFiltered = notFiltered || (!searchDocs.length && docFilters.length && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript).length); + notFiltered = notFiltered || (!searchDocs.length && (!docFilters.length && !docRangeFilters.length) && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; subDocs.forEach((t) => { const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); - notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && (!docFilters.length || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript).length)); + notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript).length)); DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); }); subDocs = newarray; @@ -243,7 +251,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: e.stopPropagation(); return added; } - else if (de.complete.annoDragData) { + else if (de.complete.annoDragData && (!this.props.isAnnotationOverlay || de.complete.annoDragData.dragDocument === this.props.Document)) { e.stopPropagation(); return this.addDocument(de.complete.annoDragData.dropDocument); } @@ -336,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); @@ -398,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], @@ -408,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; @@ -471,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(); @@ -494,4 +507,5 @@ import { SelectionManager } from "../../util/SelectionManager"; import { OverlayView } from "../OverlayView"; import { setTimeout } from "timers"; import { Hypothesis } from "../../util/HypothesisUtils"; +import { GetEffectiveAcl } from "../../../fields/util"; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 257eaf7f0..a7c8a7cdc 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -6,7 +6,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { Document } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, NumCast, ScriptCast, StrCast, Cast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, Utils } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; @@ -173,6 +173,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll focus={this.props.focus} ScreenToLocalTransform={this.titleTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.doc} ContainingCollectionView={this.props.CollectionView} @@ -205,7 +206,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.outerXf, this.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), BoolCast(this.doc.treeViewPreventOpen), [], this.props.onCheckedClick, - this.onChildClick, this.props.ignoreFields, true, this.whenActiveChanged); + this.onChildClick, this.props.ignoreFields, true, this.whenActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.dontRegisterChildViews, "boolean", null)); } @computed get titleBar() { const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index e35474b81..a27fa5a66 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -10,7 +10,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; +import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx, normalizeEmail, denormalizeEmail } from '../../../fields/util'; import { returnFalse, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -140,11 +140,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return false; } else { - if (this.props.Document[AclSym]) { + if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym])) { added.forEach(d => { for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - if (d.author === key.substring(4).replace("_", ".") && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); - else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true); + if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); + //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true); //else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); } }); @@ -157,9 +157,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus }); } else { - added.map(doc => { + added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => { // only make a pushpin if we have acl's to edit the document const context = Cast(doc.context, Doc, null); - if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { + const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); + if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ title: "pushpin", label: "", icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", @@ -186,9 +187,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - const docAcl = GetEffectiveAcl(doc); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || docAcl === AclAdmin) { - const docs = doc instanceof Doc ? [doc] : doc as Doc[]; + const indocs = doc instanceof Doc ? [doc] : doc as Doc[]; + const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); + if (docs.length) { const targetDataDoc = this.props.Document[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const toRemove = value.filter(v => docs.includes(v)); @@ -196,7 +197,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { const ind = (targetDataDoc[this.props.fieldKey] as List<Doc>).indexOf(doc); - (targetDataDoc[this.props.fieldKey] as List<Doc>).splice(ind, 0); if (ind !== -1) { Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index 13aa60e7c..087d106c5 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -17,7 +17,7 @@ import { Docs, DocumentOptions } from "../../documents/Documents"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; -import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; @@ -90,8 +90,6 @@ export interface SchemaTableProps { @observer export class SchemaTable extends React.Component<SchemaTableProps> { - private DIVIDER_WIDTH = 4; - @observable _cellIsEditing: boolean = false; @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; @observable _openCollections: Set<number> = new Set; @@ -104,7 +102,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } - @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } + @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth(); } @computed get childDocs() { if (this.props.childDocs) return this.props.childDocs; @@ -346,9 +344,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col); - const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); - pdoc && this.props.setPreviewDoc(pdoc); - e.stopPropagation(); + if (direction) { + const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); + pdoc && this.props.setPreviewDoc(pdoc); + e.stopPropagation(); + } } else if (e.keyCode === 27) { this.props.setPreviewDoc(undefined); e.stopPropagation(); // stopPropagation for left/right arrows @@ -580,6 +580,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { PanelHeight={() => 150} ScreenToLocalTransform={this.getPreviewTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} 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 1effed643..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,13 +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; - 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; } @@ -61,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()) { @@ -120,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) { - if (unpin) console.log('remove unpin'); + 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(); } } @@ -188,34 +201,46 @@ 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) { const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0]?.firstChild as HTMLElement); const scale = Utils.GetScreenTransform(this._mainCont).scale; - return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); + return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.ContentScaling() / scale); } return Transform.Identity(); } - @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 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); } // adds a tab to the layout based on the locaiton parameter which can be: @@ -263,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(); @@ -301,6 +326,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { addDocTab={this.addDocTab} pinToPres={TabDocView.PinDoc} docFilters={CollectionDockingView.Instance.docFilters} + docRangeFilters={CollectionDockingView.Instance.docRangeFilters} searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} fitToBox={true} /> @@ -310,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; @@ -336,9 +363,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { rootSelected={returnTrue} addDocument={undefined} removeDocument={undefined} - ContentScaling={this.contentScaling} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} + ContentScaling={this.ContentScaling} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} NativeHeight={this.nativeHeight() ? this.nativeHeight : undefined} NativeWidth={this.nativeWidth() ? this.nativeWidth : undefined} ScreenToLocalTransform={this.ScreenToLocalTransform} @@ -350,15 +377,18 @@ export class TabDocView extends React.Component<TabDocViewProps> { addDocTab={this.addDocTab} pinToPres={TabDocView.PinDoc} docFilters={CollectionDockingView.Instance.docFilters} + docRangeFilters={CollectionDockingView.Instance.docRangeFilters} searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> - {this._document._viewType !== CollectionViewType.Freeform || this._document.hideMinimap ? (null) : this.renderMiniMap()} - <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; })} > - <FontAwesomeIcon icon={"globe-asia"} size="lg" /> - </div> - </Tooltip> + {this._document._viewType !== CollectionViewType.Freeform ? (null) : + <>{this._document.hideMinimap ? (null) : this.renderMiniMap()} + <Tooltip key="ttip" 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; })} > + <FontAwesomeIcon icon={"globe-asia"} size="lg" /> + </div> + </Tooltip> + </>} </>; } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 486e44f6b..925eb4be6 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -48,6 +48,7 @@ export interface TreeViewProps { indentDocument?: () => void; outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; + dontRegisterView?: boolean; backgroundColor?: (doc: Opt<Doc>, renderDepth: number) => string | undefined; outerXf: () => { translateX: number, translateY: number }; treeView: CollectionTreeView; @@ -184,7 +185,7 @@ export class TreeView extends React.Component<TreeViewProps> { } public static makeTextBullet() { - const bullet = Docs.Create.TextDocument("-text-", { title: "-title-", forceActive: true, _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewOutlineMode: true, x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 }); + const bullet = Docs.Create.TextDocument("-text-", { title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, forceActive: true, _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewOutlineMode: true, x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 }); Doc.GetProto(bullet).layout = CollectionView.LayoutString("data"); Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text'); Doc.GetProto(bullet).data = new List<Doc>([]); @@ -278,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); })())); } @@ -319,7 +320,7 @@ export class TreeView extends React.Component<TreeViewProps> { this.props.treeView, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged); + [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView); } else { contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} @@ -372,7 +373,7 @@ export class TreeView extends React.Component<TreeViewProps> { this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged)} + [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView)} </ul >; } else if (this.treeViewExpandedView === "fields") { return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > @@ -387,6 +388,7 @@ export class TreeView extends React.Component<TreeViewProps> { Document={this.doc} DataDoc={undefined} LibraryPath={emptyPath} + dontRegisterView={this.props.dontRegisterView} renderDepth={this.props.renderDepth + 1} rootSelected={returnTrue} treeViewDoc={undefined} @@ -400,6 +402,7 @@ export class TreeView extends React.Component<TreeViewProps> { focus={returnFalse} ScreenToLocalTransform={this.docTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.containingCollection} ContainingCollectionView={undefined} @@ -513,6 +516,7 @@ export class TreeView extends React.Component<TreeViewProps> { bringToFront={emptyFunction} dontRegisterView={BoolCast(this.props.treeView.props.Document.dontRegisterChildViews)} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={this.props.containingCollection} @@ -583,6 +587,7 @@ export class TreeView extends React.Component<TreeViewProps> { focus={this.refocus} ScreenToLocalTransform={this.docTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.containingCollection} ContainingCollectionView={undefined} @@ -658,7 +663,8 @@ export class TreeView extends React.Component<TreeViewProps> { onChildClick: undefined | (() => ScriptField), ignoreFields: string[] | undefined, firstLevel: boolean, - whenActiveChanged: (isActive: boolean) => void) { + whenActiveChanged: (isActive: boolean) => void, + dontRegisterView: boolean | undefined) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); @@ -709,7 +715,7 @@ export class TreeView extends React.Component<TreeViewProps> { } }; const outdent = !parentCollectionDoc ? undefined : () => { - if (remove && StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { + if (parentCollectionDoc._viewType === CollectionViewType.Tree && remove && StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { const fieldKeysub = StrCast(parentCollectionDoc.layout).split('fieldKey')[1]; const fieldKey = fieldKeysub.split("\'")[1]; remove(child); @@ -722,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]} @@ -742,6 +748,7 @@ export class TreeView extends React.Component<TreeViewProps> { panelWidth={rowWidth} panelHeight={rowHeight} ChromeHeight={ChromeHeight} + dontRegisterView={dontRegisterView} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} 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/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 79a540210..4cf257640 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -22,74 +22,63 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo @observable _start = 0; _anchorDisposer: IReactionDisposer | undefined; _timeout: NodeJS.Timeout | undefined; - componentWillUnmount() { - this._anchorDisposer?.(); - } - @action - timeout = () => (Date.now() < this._start++ + 1000) && setTimeout(this.timeout, 25) + componentWillUnmount() { this._anchorDisposer?.(); } + @action timeout = action(() => (Date.now() < this._start++ + 1000) && (this._timeout = setTimeout(this.timeout, 25))); componentDidMount() { - this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform(), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document)], + this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform()], action(() => { this._start = Date.now(); this._timeout && clearTimeout(this._timeout); this._timeout = setTimeout(this.timeout, 25); - if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv) return; - setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() - setTimeout(action(() => (!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. - const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; - const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; - const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv); - const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv); - const a = adiv.getBoundingClientRect(); - const b = bdiv.getBoundingClientRect(); - const abounds = adiv.parentElement!.getBoundingClientRect(); - const bbounds = bdiv.parentElement!.getBoundingClientRect(); - const apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height, - bbounds.left, bbounds.top, bbounds.width, bbounds.height, - a.left + a.width / 2, a.top + a.height / 2); - const bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height, - abounds.left, abounds.top, abounds.width, abounds.height, - apt.point.x, apt.point.y); - const afield = this.props.A.props.LayoutTemplateString?.indexOf("anchor1") === -1 ? "anchor2" : "anchor1"; - const bfield = afield === "anchor1" ? "anchor2" : "anchor1"; - - // really hacky stuff to make the LinkAnchorBox display where we want it to: - // if there's an element in the DOM with a classname containing the link's id and a data-targetids attribute containing the other end of the link, - // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right - // otherwise, we just use the computed nearest point on the document boundary to the target Document - const linkId = this.props.LinkDocs[0][Id]; // this link's Id - const AanchorId = (this.props.LinkDocs[0][afield] as Doc)[Id]; // anchor a's id - const BanchorId = (this.props.LinkDocs[0][bfield] as Doc)[Id]; // anchor b's id - const linkEles = Array.from(window.document.getElementsByClassName(linkId)); - const targetAhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(AanchorId)); - const targetBhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(BanchorId)); - if (!targetBhyperlink) { - this.props.A.rootDoc[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; - this.props.A.rootDoc[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; - } else { - setTimeout(() => { - (this.props.A.rootDoc[(this.props.A.props as any).fieldKey] as Doc); - const m = targetBhyperlink.getBoundingClientRect(); - const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.A.rootDoc[afield + "_x"] = Math.min(1, mp[0] / this.props.A.props.PanelWidth()) * 100; - this.props.A.rootDoc[afield + "_y"] = Math.min(1, mp[1] / this.props.A.props.PanelHeight()) * 100; - }, 0); - } - if (!targetAhyperlink) { - this.props.A.rootDoc[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; - this.props.A.rootDoc[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; - } else { - setTimeout(() => { - (this.props.B.rootDoc[(this.props.B.props as any).fieldKey] as Doc); - const m = targetAhyperlink.getBoundingClientRect(); - const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.B.rootDoc[bfield + "_x"] = Math.min(1, mp[0] / this.props.B.props.PanelWidth()) * 100; - this.props.B.rootDoc[bfield + "_y"] = Math.min(1, mp[1] / this.props.B.props.PanelHeight()) * 100; - }, 0); - } + setTimeout(this.placeAnchors); }) , { fireImmediately: true }); } + placeAnchors = () => { + const { A, B, LinkDocs } = this.props; + const linkDoc = LinkDocs[0]; + if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return; + setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() + setTimeout(action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. + const acont = A.props.Document.type === DocumentType.LINK ? A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const bcont = B.props.Document.type === DocumentType.LINK ? B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const adiv = (acont.length ? acont[0] : A.ContentDiv); + const bdiv = (bcont.length ? bcont[0] : B.ContentDiv); + const a = adiv.getBoundingClientRect(); + const b = bdiv.getBoundingClientRect(); + const { left: aleft, top: atop, width: awidth, height: aheight } = adiv.parentElement!.getBoundingClientRect(); + const { left: bleft, top: btop, width: bwidth, height: bheight } = bdiv.parentElement!.getBoundingClientRect(); + const apt = Utils.closestPtBetweenRectangles(aleft, atop, awidth, aheight, bleft, btop, bwidth, bheight, a.left + a.width / 2, a.top + a.height / 2); + const bpt = Utils.closestPtBetweenRectangles(bleft, btop, bwidth, bheight, aleft, atop, awidth, aheight, apt.point.x, apt.point.y); + const afield = A.props.LayoutTemplateString?.indexOf("anchor1") === -1 ? "anchor2" : "anchor1"; + const bfield = afield === "anchor1" ? "anchor2" : "anchor1"; + + // really hacky stuff to make the LinkAnchorBox display where we want it to: + // if there's an element in the DOM with a classname containing the link's id and a data-targetids attribute containing the other end of the link, + // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right + // otherwise, we just use the computed nearest point on the document boundary to the target Document + const linkEles = Array.from(window.document.getElementsByClassName(linkDoc[Id])); + const targetAhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes((linkDoc[afield] as Doc)[Id])); + const targetBhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes((linkDoc[bfield] as Doc)[Id])); + if (!targetBhyperlink) { + A.rootDoc[afield + "_x"] = (apt.point.x - aleft) / awidth * 100; + A.rootDoc[afield + "_y"] = (apt.point.y - atop) / aheight * 100; + } else { + const m = targetBhyperlink.getBoundingClientRect(); + const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + A.rootDoc[afield + "_x"] = Math.min(1, mp[0] / A.props.PanelWidth()) * 100; + A.rootDoc[afield + "_y"] = Math.min(1, mp[1] / A.props.PanelHeight()) * 100; + } + if (!targetAhyperlink) { + B.rootDoc[bfield + "_x"] = (bpt.point.x - bleft) / bwidth * 100; + B.rootDoc[bfield + "_y"] = (bpt.point.y - btop) / bheight * 100; + } else { + const m = targetAhyperlink.getBoundingClientRect(); + const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + B.rootDoc[bfield + "_x"] = Math.min(1, mp[0] / B.props.PanelWidth()) * 100; + B.rootDoc[bfield + "_y"] = Math.min(1, mp[1] / B.props.PanelHeight()) * 100; + } + } pointerDown = (e: React.PointerEvent) => { @@ -111,51 +100,68 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo let rect = el.getBoundingClientRect(); const top = rect.top, height = rect.height; var el = el.parentNode; - do { - rect = el.getBoundingClientRect(); - if (top <= rect.bottom === false && getComputedStyle(el).overflow === "hidden") return rect.bottom; - // Check if the element is out of view due to a container scrolling - if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top; + while (el && el !== document.body) { + if (el.hasOwnProperty("getBoundingClientRect")) { + rect = el.getBoundingClientRect(); + if (top <= rect.bottom === false && getComputedStyle(el).overflow === "hidden") return rect.bottom; + // Check if the element is out of view due to a container scrolling + if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top; + } el = el.parentNode; - } while (el !== document.body); + } // Check its within the document viewport return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; } - - @computed get renderData() { - this._start; - if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv || !this.props.LinkDocs.length) { - return undefined; + visibleX = (el: any) => { + let rect = el.getBoundingClientRect(); + const left = rect.left, width = rect.width; + var el = el.parentNode; + while (el && el !== document.body) { + if (el.hasOwnProperty("getBoundingClientRect")) { + rect = el.getBoundingClientRect(); + if (left <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.right; + // Check if the element is out of view due to a container scrolling + if ((left + width) <= rect.left && getComputedStyle(el).overflow === "hidden") return rect.left; + } + el = el.parentNode; } - this.props.A.props.ScreenToLocalTransform().transform(this.props.B.props.ScreenToLocalTransform()); - const acont = this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const bcont = this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv); - const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv); + // Check its within the document viewport + return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; + } + + @computed.struct get renderData() { + this._start; SnappingManager.GetIsDragging(); + const { A, B, LinkDocs } = this.props; + if (!A.ContentDiv || !B.ContentDiv || !LinkDocs.length) return undefined; + const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const adiv = (acont.length ? acont[0] : A.ContentDiv); + const bdiv = (bcont.length ? bcont[0] : B.ContentDiv); + for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; + for (let apdiv = bdiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; const a = adiv.getBoundingClientRect(); const b = bdiv.getBoundingClientRect(); const atop = this.visibleY(adiv); const btop = this.visibleY(bdiv); - const apt = Utils.closestPtBetweenRectangles(a.left, atop, a.width, a.height, - b.left, btop, b.width, b.height, - a.left + a.width / 2, a.top + a.height / 2); - const bpt = Utils.closestPtBetweenRectangles(b.left, btop, b.width, b.height, - a.left, atop, a.width, a.height, - apt.point.x, apt.point.y); + const aleft = this.visibleX(adiv); + const bleft = this.visibleX(bdiv); + const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top; + const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); + const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, aleft, atop, a.width, a.height, apt.point.x, apt.point.y); const pt1 = [apt.point.x, apt.point.y]; const pt2 = [bpt.point.x, bpt.point.y]; - const pt1vec = [pt1[0] - (a.left + a.width / 2), pt1[1] - (atop + a.height / 2)]; - const pt2vec = [pt2[0] - (b.left + b.width / 2), pt2[1] - (btop + b.height / 2)]; + const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)]; + const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)]; const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; - const pt1norm = [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; - const pt2norm = [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; - const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - const bActive = this.props.B.isSelected() || Doc.IsBrushed(this.props.B.props.Document); + const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; + const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; + const aActive = A.isSelected() || Doc.IsBrushed(A.props.Document); + const bActive = B.isSelected() || Doc.IsBrushed(B.props.Document); - const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetX); - const textY = (pt1[1] + pt2[1]) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetY); + const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX); + const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY); return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 }; } @@ -165,9 +171,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> - <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > + {textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > {StrCast(this.props.LinkDocs[0].description)} - </text> + </text>} </>); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 1a2421bfd..6a1a41ca7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,4 +1,4 @@ -import { computed } from "mobx"; +import { computed, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; @@ -10,12 +10,10 @@ import React = require("react"); import { Utils, emptyFunction } from "../../../../Utils"; import { DocumentType } from "../../../documents/DocumentTypes"; import { SnappingManager } from "../../../util/SnappingManager"; -import { Cast } from "../../../../fields/Types"; @observer export class CollectionFreeFormLinksView extends React.Component { - @computed - get uniqueConnections() { + @computed get uniqueConnections() { const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { if (!drawnPairs.reduce((found, drawnPair) => { const match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b); @@ -37,7 +35,7 @@ export class CollectionFreeFormLinksView extends React.Component { } render() { - return SnappingManager.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container"> + return <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas"> {this.uniqueConnections} </svg> 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 8af048d67..1033050b9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -7,7 +7,7 @@ import { Id } from "../../../../fields/FieldSymbols"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; -import { createSchema, makeInterface } from "../../../../fields/Schema"; +import { createSchema, makeInterface, listSpec } from "../../../../fields/Schema"; import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; @@ -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", @@ -88,6 +89,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private _clusterDistance: number = 75; private _hitCluster = false; private _layoutComputeReaction: IReactionDisposer | undefined; + private _boundsReaction: IReactionDisposer | undefined; private _layoutPoolData = new ObservableMap<string, PoolData>(); private _layoutSizeData = new ObservableMap<string, { width?: number, height?: number }>(); private _cachedPool: Map<string, PoolData> = new Map(); @@ -105,27 +107,31 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @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 parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } + @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; } - private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; - private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; - private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ? - Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), - this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : - NumCast(this.Document[this.scaleFieldKey], 1)) - + private panX = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; + private panY = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; + private zoomScaling = () => { + const mult = this.fitToContentScaling / this.parentScaling; + 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 * zs; + } + return mult * NumCast(this.Document[this.scaleFieldKey], 1); + } @computed get cachedCenteringShiftX(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections + return !this.props.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections + return !this.props.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -163,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) { @@ -197,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); @@ -214,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 @@ -776,15 +793,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action zoom = (pointX: number, pointY: number, deltaY: number): void => { let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05; - if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { - deltaScale = 1 / this.zoomScaling(); - } if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.getTransform().transformPoint(pointX, pointY); + const invTransform = this.getLocalTransform().inverse(); + if (deltaScale * invTransform.Scale > 20) { + deltaScale = 20 / invTransform.Scale; + } + if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) { + deltaScale = 1 / invTransform.Scale; + } const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { - const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); + const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 20); this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); } @@ -792,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(); } @@ -828,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)); @@ -866,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) => { + 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 @@ -885,48 +906,72 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P SelectionManager.DeselectAll(); if (this.props.Document.scrollHeight) { const annotOn = Cast(doc.annotationOn, Doc) as Doc; + let delay = 1000; if (!annotOn) { - this.props.focus(doc); + !dontCenter && this.props.focus(doc); + afterFocus && setTimeout(afterFocus, delay); } else { const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height); - const offset = annotOn && (contextHgt / 2); - this.props.Document._scrollY = NumCast(doc.y) - offset; + const curScroll = NumCast(this.props.Document._scrollTop); + let scrollTo = curScroll; + if (curScroll + contextHgt < NumCast(doc.y)) { + scrollTo = NumCast(doc.y) + Math.max(NumCast(doc._height), 100) - contextHgt; + } else if (curScroll > NumCast(doc.y)) { + scrollTo = Math.max(0, NumCast(doc.y) - 50); + } + if (curScroll !== scrollTo || this.props.Document._viewTransition) { + this.props.Document._scrollPreviewY = this.props.Document._scrollY = scrollTo; + delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0; + !dontCenter && this.props.focus(this.props.Document); + afterFocus && setTimeout(afterFocus, delay); + } else { + !dontCenter && delay && this.props.focus(this.props.Document); + afterFocus?.(!dontCenter && delay ? true : false); + + } } - afterFocus && setTimeout(afterFocus, 1000); } else { const layoutdoc = Doc.Layout(doc); - const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2; - const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2; + 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 ? (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); - const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; - - if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { + if (DocListCast(this.dataDoc[this.props.annotationsKey || this.props.fieldKey]).includes(doc)) { // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... 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); - willZoom && this.setScaleToZoom(layoutdoc, scale); - Doc.linkFollowHighlight(doc); - afterFocus && setTimeout(() => { - if (afterFocus?.()) { - this.Document._panX = savedState.px; - this.Document._panY = savedState.py; - this.Document[this.scaleFieldKey] = savedState.s; - this.Document._viewTransition = savedState.pt; - } - }, 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) => { - this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); + 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))); } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } @@ -949,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, @@ -964,6 +1008,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.Document, docFilters: this.docFilters, + docRangeFilters: this.docRangeFilters, searchFilterDocs: this.searchFilterDocs, focus: this.focusDocument, backgroundColor: this.getClusterColor, @@ -1105,7 +1150,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); - const elements: ViewDefResult[] = computedElementData.slice(); + const elements = computedElementData.slice(); const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach(entry => elements.push({ @@ -1126,8 +1171,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); - if (this.props.isAnnotationOverlay) { - this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); + if (this.props.isAnnotationOverlay) { // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar + if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, NumCast(this.props.Document[this.scaleFieldKey], 1)); + else this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); @@ -1140,12 +1186,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); + if (!this.props.annotationsKey) { + this._boundsReaction = reaction(() => this.contentBounds, + bounds => (!this.fitToContent && this._layoutElements?.length) && setTimeout(() => { + const rbounds = Cast(this.Document._renderContentBounds, listSpec("number"), [0, 0, 0, 0]); + if (rbounds[0] !== bounds.x || rbounds[1] !== bounds.y || rbounds[2] !== bounds.r || rbounds[3] !== bounds.b) { + this.Document._renderContentBounds = new List<number>([bounds.x, bounds.y, bounds.r, bounds.b]); + } + })); + } this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } componentWillUnmount() { this._layoutComputeReaction?.(); + this._boundsReaction?.(); this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } @@ -1154,7 +1210,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onCursorMove = (e: React.PointerEvent) => { - super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); + // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } @@ -1165,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(); @@ -1255,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" }); } @@ -1381,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} @@ -1394,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} @@ -1410,19 +1505,20 @@ 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; } @computed get backgroundEvents() { return this.layoutDoc._isBackground && SnappingManager.GetIsDragging(); } + render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); - !this.fitToContent && this._layoutElements?.length && setTimeout(() => this.Document._renderContentBounds = new List<number>([this.contentBounds.x, this.contentBounds.y, this.contentBounds.r, this.contentBounds.b]), 0); return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onPointerOver={this.onPointerOver} onWheel={this.onPointerWheel} @@ -1582,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() { @@ -1613,6 +1709,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF </>; } + render() { // trace(); const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); @@ -1622,6 +1719,12 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const pany = -this.props.panY(); const zoom = this.props.zoomScaling(); return <div className={freeformclass} + onScroll={e => { + const target = e.target as any; + if (getComputedStyle(target)?.overflow === "visible") { // if collection is visible, then scrolling will mess things up since there are no scroll bars + target.scrollTop = target.scrollLeft = 0; + } + }} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, transition: this.props.transition, 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 e1ebb4eee..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; @@ -141,7 +141,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else if (!e.ctrlKey && !e.metaKey) { FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout && !this.props.ChildLayoutString ? e.key : ""; FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); - this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100)); + this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xMargin === 0)); e.stopPropagation(); } } @@ -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 0e4785d36..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} @@ -233,6 +232,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} @@ -289,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/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 9a4fb0627..c5e1816d9 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -232,6 +232,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 1539d1399..b2ea87c06 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -33,7 +33,8 @@ $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; $DFLT_IMAGE_NATIVE_DIM: 900px; @@ -41,6 +42,7 @@ $MENU_PANEL_WIDTH: 60px; :export { contextMenuZindex: $contextMenu-zindex; + SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH; COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH; MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts index fb3a327d0..c56b75d5e 100644 --- a/src/client/views/globalCssVariables.scss.d.ts +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -1,6 +1,7 @@ interface IGlobalScss { contextMenuZindex: string; // context menu shows up over everything + SCHEMA_DIVIDER_WIDTH: string; COLLECTION_BORDER_WIDTH: string; MINIMIZED_ICON_SIZE: string; MAX_ROW_HEIGHT: string; diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 3713a1026..435b9d904 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -10,264 +10,6 @@ import { undoBatch } from "../../util/UndoManager"; import './LinkEditor.scss'; import React = require("react"); -interface GroupTypesDropdownProps { - groupType: string; - setGroupType: (group: string) => void; -} -// this dropdown could be generalized -@observer -class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> { - @observable private _searchTerm: string = this.props.groupType; - @observable private _groupType: string = this.props.groupType; - @observable private _isEditing: boolean = false; - - @action - createGroup = (groupType: string): void => { - this.props.setGroupType(groupType); - LinkManager.Instance.addGroupType(groupType); - } - - @action - onChange = (val: string): void => { - this._searchTerm = val; - this._groupType = val; - this._isEditing = true; - } - - @action - onKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === "Enter") { - const allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes()); - const groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()); - - if (exactFound > -1) { - const groupType = groupOptions[exactFound]; - this.props.setGroupType(groupType); - this._groupType = groupType; - } else { - this.createGroup(this._searchTerm); - this._groupType = this._searchTerm; - } - - this._searchTerm = this._groupType; - this._isEditing = false; - } - } - - @action - onOptionClick = (value: string, createNew: boolean): void => { - if (createNew) { - this.createGroup(this._searchTerm); - this._groupType = this._searchTerm; - - } else { - this.props.setGroupType(value); - this._groupType = value; - - } - this._searchTerm = this._groupType; - this._isEditing = false; - } - - @action - onButtonPointerDown = (): void => { - this._isEditing = true; - } - - renderOptions = (): JSX.Element[] | JSX.Element => { - if (this._searchTerm === "") return <></>; - - const allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes()); - const groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1; - - const options = groupOptions.map(groupType => { - const ref = React.createRef<HTMLDivElement>(); - return <div key={groupType} ref={ref} className="linkEditor-option" - onClick={() => this.onOptionClick(groupType, false)}>{groupType}</div>; - }); - - // if search term does not already exist as a group type, give option to create new group type - if (!exactFound && this._searchTerm !== "") { - const ref = React.createRef<HTMLDivElement>(); - options.push(<div key={""} ref={ref} className="linkEditor-option" - onClick={() => this.onOptionClick(this._searchTerm, true)}>Define new "{this._searchTerm}" relationship</div>); - } - - return options; - } - - render() { - if (this._isEditing || this._groupType === "") { - return ( - <div className="linkEditor-dropdown"> - <input type="text" value={this._groupType === "-ungrouped-" ? "" : this._groupType} placeholder="Search for or create a new group" - onChange={e => this.onChange(e.target.value)} onKeyDown={this.onKeyDown} autoFocus></input> - <div className="linkEditor-options-wrapper"> - {this.renderOptions()} - </div> - </div > - ); - } else { - return <button className="linkEditor-typeButton" onClick={() => this.onButtonPointerDown()}>{this._groupType}</button>; - } - } -} - - -interface LinkMetadataEditorProps { - id: string; - groupType: string; - mdDoc: Doc; - mdKey: string; - mdValue: string; - changeMdIdKey: (id: string, newKey: string) => void; -} -@observer -class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> { - @observable private _key: string = this.props.mdKey; - @observable private _value: string = this.props.mdValue; - @observable private _keyError: boolean = false; - - @action - setMetadataKey = (value: string): void => { - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); - - // don't allow user to create existing key - const newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase()); - if (newIndex > -1) { - this._keyError = true; - this._key = value; - return; - } else { - this._keyError = false; - } - - // set new value for key - const currIndex = groupMdKeys.findIndex(key => { - return StrCast(key).toUpperCase() === this._key.toUpperCase(); - }); - if (currIndex === -1) console.error("LinkMetadataEditor: key was not found"); - groupMdKeys[currIndex] = value; - - this.props.changeMdIdKey(this.props.id, value); - this._key = value; - LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]); - } - - @action - setMetadataValue = (value: string): void => { - if (!this._keyError) { - this._value = value; - Doc.GetProto(this.props.mdDoc)[this._key] = value; - } - } - - @action - removeMetadata = (): void => { - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); - - const index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase()); - if (index === -1) console.error("LinkMetadataEditor: key was not found"); - groupMdKeys.splice(index, 1); - - LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys); - this._key = ""; - } - - render() { - return ( - <div className="linkEditor-metadata-row"> - <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>: - <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input> - <button title="remove metadata from relationship" onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button> - </div> - ); - } -} - -interface LinkGroupEditorProps { - sourceDoc: Doc; - linkDoc: Doc; - groupDoc: Doc; -} -@observer -export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { - - private _metadataIds: Map<string, string> = new Map(); - - constructor(props: LinkGroupEditorProps) { - super(props); - - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.linkRelationship)); - groupMdKeys.forEach(key => this._metadataIds.set(key, Utils.GenerateGuid())); - } - - @action - setGroupType = (groupType: string): void => { - Doc.GetProto(this.props.groupDoc).linkRelationship = groupType; - } - - removeGroupFromLink = (groupType: string): void => { - LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType); - } - - deleteGroup = (groupType: string): void => { - LinkManager.Instance.deleteGroupType(groupType); - } - - - @action - addMetadata = (groupType: string): void => { - this._metadataIds.set("new key", Utils.GenerateGuid()); - const mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - // only add "new key" if there is no other key with value "new key"; prevents spamming - if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key"); - LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys); - } - - // for key rendering purposes - changeMdIdKey = (id: string, newKey: string) => { - this._metadataIds.set(newKey, id); - } - - renderMetadata = (): JSX.Element[] => { - const metadata: Array<JSX.Element> = []; - const groupDoc = this.props.groupDoc; - const groupType = StrCast(groupDoc.linkRelationship); - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - - groupMdKeys.forEach((key) => { - const val = StrCast(groupDoc[key]); - metadata.push( - <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={groupDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} /> - ); - }); - return metadata; - } - - render() { - const groupType = StrCast(this.props.groupDoc.linkRelationship); - // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") { - const buttons = <button className="linkEditor-button" disabled={groupType === ""} onClick={() => this.deleteGroup(groupType)} title="Delete Relationship from all links"><FontAwesomeIcon icon="trash" size="sm" /></button>; - const addButton = <button className="linkEditor-addbutton" onClick={() => this.addMetadata(groupType)} disabled={groupType === ""} title="Add metadata to relationship"><FontAwesomeIcon icon="plus" size="sm" /></button>; - - return ( - <div className="linkEditor-group"> - <div className="linkEditor-group-row "> - {buttons} - <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} /> - <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove relationship from link"><FontAwesomeIcon icon="times" size="sm" /></button> - </div> - {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>} - {addButton} - {this.renderMetadata()} - </div> - ); - } -} - interface LinkEditorProps { sourceDoc: Doc; @@ -422,10 +164,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> { render() { const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); - const groups = [this.props.linkDoc].map(groupDoc => { - return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc} - sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />; - }); return !destination ? (null) : ( <div className="linkEditor"> @@ -450,8 +188,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div>{this.editDescription}</div> <div>{this.followingDropdown}</div> - - {/* {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} */} </div> ); 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 29e1d921c..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,45 +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(); - } - - viewGroupAsTable = (groupType: string): JSX.Element => { - const keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - const index = keys.indexOf(""); - if (index > -1) keys.splice(index, 1); - const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); - const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); - const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table", childDropAction: "alias" })); - const ref = React.createRef<HTMLDivElement>(); - return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; - } - render() { const set = new Set<Doc>(this.props.group); const groupItems = Array.from(set.keys()).map(linkDoc => { @@ -86,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 d3fbf39a2..5c9123876 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -90,19 +90,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return true; } - renderMetadata = (): JSX.Element => { - const index = StrCast(this.props.linkDoc.title).toUpperCase() === this.props.groupType.toUpperCase() ? 0 : -1; - const mdDoc = index > -1 ? this.props.linkDoc : undefined; - - let mdRows: Array<JSX.Element> = []; - if (mdDoc) { - const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); - mdRows = keys.map(key => <div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>); - } - - return (<div className="link-metadata">{mdRows}</div>); - } - @action onLinkButtonDown = (e: React.PointerEvent): void => { this._downX = e.clientX; @@ -122,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(); } @@ -146,14 +134,14 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } - @action.bound + + @action public static followDefault(linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc, addDocTab: (doc: Doc, where: string) => void) { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; if (linkDoc.followLinkLocation === "openExternal" && destinationDoc.type === DocumentType.WEB) { window.open(`${StrCast(linkDoc.annotationUri)}#annotations:${StrCast(linkDoc.annotationId)}`, '_blank'); - return; } if (linkDoc.followLinkLocation && linkDoc.followLinkLocation !== "default") { @@ -166,7 +154,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { }); } } else { - DocumentManager.Instance.FollowLink(linkDoc, sourceDoc, doc => addDocTab(doc, "add:right"), false); + DocumentManager.Instance.FollowLink(linkDoc, sourceDoc, addDocTab, false); } linkDoc.linksToAnnotation && Hypothesis.scrollToAnnotation(StrCast(linkDoc.annotationId), destinationDoc); @@ -191,8 +179,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { } render() { - const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); - const canExpand = keys ? keys.length > 0 : false; const eyeIcon = this.props.linkDoc.hidden ? "eye-slash" : "eye"; @@ -230,7 +216,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return ( <div className="linkMenu-item"> - <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}> + <div className={"linkMenu-item-content expand-two"}> <div ref={this._drag} className="linkMenu-name" //title="drag to view target. click to customize." onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} @@ -243,22 +229,19 @@ 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={() => LinkMenuItem.followDefault(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc, this.props.addDocTab)}> + <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} > - {canExpand ? <div title="Show more" className="button" onPointerDown={e => this.toggleShowMore(e)}> - <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>} <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show link" : "Hide link"}</div></>}> <div className="button" ref={this._editRef} onPointerDown={this.showLink}> @@ -277,7 +260,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */} </div> </div> - {this._showMore ? this.renderMetadata() : <></>} </div> </div > diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index d6f8ce19c..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); @@ -65,7 +65,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF w: Cast(doc["w-indexed"], listSpec("number"), [NumCast(doc._width)]).reduce((p, w, i) => (i <= timecode && w !== undefined) || p === undefined ? w : p, undefined as any as number), x: Cast(doc["x-indexed"], listSpec("number"), [NumCast(doc.x)]).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number), y: Cast(doc["y-indexed"], listSpec("number"), [NumCast(doc.y)]).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number), - scroll: Cast(doc["scroll-indexed"], listSpec("number"), [NumCast(doc._scrollY, 0)]).reduce((p, s, i) => (i <= timecode && s !== undefined) || p === undefined ? s : p, undefined as any as number), + scroll: Cast(doc["scroll-indexed"], listSpec("number"), [NumCast(doc._scrollTop, 0)]).reduce((p, s, i) => (i <= timecode && s !== undefined) || p === undefined ? s : p, undefined as any as number), opacity: Cast(doc["opacity-indexed"], listSpec("number"), [NumCast(doc.opacity, 1)]).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number), }); } @@ -97,24 +97,24 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF d["text-color"] = "grey"; } else { d["text-color"] = "black"; } } else if (d.appearFrame === 0) { - d["text-color"] = "black"; } } - public static updateScrollframe(doc: Doc, time: number) { - const timecode = Math.round(time); - const scrollIndexed = Cast(doc['scroll-indexed'], listSpec("number"), null); - scrollIndexed?.length <= timecode + 1 && scrollIndexed.push(undefined as any as number); - setTimeout(() => doc.dataTransition = "inherit", 1010); - } + // public static updateScrollframe(doc: Doc, time: number) { + // console.log('update scroll frame'); + // const timecode = Math.round(time); + // const scrollIndexed = Cast(doc['scroll-indexed'], listSpec("number"), null); + // scrollIndexed?.length <= timecode + 1 && scrollIndexed.push(undefined as any as number); + // setTimeout(() => doc.dataTransition = "inherit", 1010); + // } - public static setupScroll(doc: Doc, timecode: number) { - const scrollList = new List<number>(); - scrollList[timecode] = NumCast(doc._scrollY); - doc["scroll-indexed"] = scrollList; - doc.activeFrame = ComputedField.MakeFunction("self._currentFrame"); - doc._scrollY = ComputedField.MakeInterpolated("scroll", "activeFrame"); - } + // public static setupScroll(doc: Doc, timecode: number) { + // const scrollList = new List<number>(); + // scrollList[timecode] = NumCast(doc._scrollTop); + // doc["scroll-indexed"] = scrollList; + // doc.activeFrame = ComputedField.MakeFunction("self._currentFrame"); + // doc._scrollTop = ComputedField.MakeInterpolated("scroll", "activeFrame"); + // } public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) { @@ -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/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx index 91f96135f..dd254ae93 100644 --- a/src/client/views/nodes/DocHolderBox.tsx +++ b/src/client/views/nodes/DocHolderBox.tsx @@ -120,6 +120,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do DataDoc={undefined} LibraryPath={emptyPath} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} @@ -148,6 +149,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do DataDoc={undefined} LibraryPath={emptyPath} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} 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 0cb5b94f4..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>(); @@ -161,7 +161,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.AnnotationUri = undefined; //!this.props.StartLink } else if (startLink !== endLink) { - const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); + const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag", undefined, undefined, true); // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved if (endLinkView) { endLinkView._link = linkDoc; @@ -209,93 +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" + 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 3d3599476..77f63b457 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,20 +1,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, returnVal } from "../../../Utils"; +import { emptyFunction, OmitKeys, returnOne, returnTransparent, returnVal, Utils } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; import { InteractionUtils } from '../../util/InteractionUtils'; @@ -39,15 +41,15 @@ import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { RadialMenu } from './RadialMenu'; import { TaskCompletionBox } from './TaskCompletedBox'; import React = require("react"); -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; -import { RichTextField } from '../../../fields/RichTextField'; -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>; ContainingCollectionDoc: Opt<Doc>; docFilters: () => string[]; + docRangeFilters: () => string[]; searchFilterDocs: () => Doc[]; FreezeDimensions?: boolean; NativeWidth?: () => number; @@ -81,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; @@ -128,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); } @@ -208,7 +210,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); if (!BoolCast(this.rootDoc.dontRegisterView, this.props.dontRegisterView)) { - DocumentManager.Instance.DocumentViews.push(this); + DocumentManager.Instance.AddView(this); } } @@ -234,8 +236,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._holdDisposer?.(); Doc.UnBrushDoc(this.props.Document); if (!this.props.dontRegisterView) { - const index = DocumentManager.Instance.DocumentViews.indexOf(this); - index !== -1 && DocumentManager.Instance.DocumentViews.splice(index, 1); + DocumentManager.Instance.RemoveView(this); } } @@ -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(); @@ -366,7 +363,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right"); } else if (this.allLinks && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { - this.allLinks.length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); + this.allLinks.length && DocumentView.followLinkClick(undefined, this.props.Document, this.props, e.shiftKey, e.altKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template @@ -384,27 +381,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // follows a link - if the target is on screen, it highlights/pans to it. // 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); - followLinkClick = async (altKey: boolean, ctrlKey: boolean, shiftKey: boolean) => { + public static followLinkClick = async (linkDoc: Opt<Doc>, sourceDoc: Doc, docView: { + focus: DocFocusFunc, + addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean, + ContainingCollectionDoc?: Doc + }, shiftKey: boolean, altKey: boolean) => { const batch = UndoManager.StartBatch("follow link click"); // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => { const targetFocusAfterDocFocus = () => { - const where = StrCast(this.Document.followLinkLocation) || followLoc; + const where = StrCast(sourceDoc.followLinkLocation) || followLoc; const hackToCallFinishAfterFocus = () => { finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. return false; // we must return false here so that the zoom to the document is not reversed. If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false }; - this.props.addDocTab(doc, where) && this.props.focus(doc, BoolCast(this.Document.followLinkZoom, true), undefined, hackToCallFinishAfterFocus); // add the target and focus on it. - return where !== "inPlace"; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target) + const addTab = docView.addDocTab(doc, where); + addTab && setTimeout(() => { + const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); + targDocView?.props.focus(doc, BoolCast(sourceDoc.followLinkZoom, false), undefined, hackToCallFinishAfterFocus); + }); // add the target and focus on it. + return where !== "inPlace" || addTab; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target) }; - if (!this.Document.followLinkZoom) { + if (!sourceDoc.followLinkZoom) { targetFocusAfterDocFocus(); } else { // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target - this.props.focus(this.props.Document, BoolCast(this.Document.followLinkZoom, true), 1, targetFocusAfterDocFocus); + docView.focus(sourceDoc, BoolCast(sourceDoc.followLinkZoom, true), 1, targetFocusAfterDocFocus); } }; - await DocumentManager.Instance.FollowLink(undefined, this.props.Document, createViewFunc, shiftKey, this.props.ContainingCollectionDoc, batch.end, altKey ? true : undefined); + await DocumentManager.Instance.FollowLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docView.ContainingCollectionDoc, batch.end, altKey ? true : undefined); } handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { @@ -415,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(); @@ -430,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)) { @@ -486,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(); @@ -497,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; @@ -511,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; @@ -546,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 } @@ -565,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); @@ -732,6 +737,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), title: StrCast(this.props.Document.title) + ".portal" }); DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to"); } + this.Document.followLinkLocation = "inPlace"; this.Document.followLinkZoom = true; this.Document.isLinkButton = true; } @@ -745,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); } } @@ -810,14 +816,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: this.toggleRaiseWhenDragged, icon: "expand-arrows-alt" }); !zorders && cm.addItem({ description: "ZOrder...", subitems: zorderItems, icon: "compass" }); + onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); + if (!this.Document.annotationOn) { const options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; !this.props.treeViewDoc && this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform && optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); - onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); @@ -879,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. }); } @@ -897,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") { @@ -910,13 +917,13 @@ 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() { - const pos = this.props.relative ? "relative " : "absolute"; TraceMobx(); return (<div className="documentView-contentsView" style={{ pointerEvents: this.props.contentsPointerEvents as any, borderRadius: "inherit", width: "100%", height: "100%" }}> <DocumentContentsView key={1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} @@ -979,9 +986,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; - @computed.struct get directLinks() { return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } - @computed.struct get allLinks() { return DocListCast(this.Document.links); } - @computed.struct get allAnchors() { + @computed get directLinks() { TraceMobx(); return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } + @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } + @computed get allAnchors() { TraceMobx(); if (this.props.LayoutTemplateString?.includes("LinkAnchorBox")) return null; if ((this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots @@ -1041,14 +1048,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const titleView = (!this.ShowTitle ? (null) : <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{ position: this.showOverlappingTitle ? "absolute" : "relative", - background: SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.userColor || (this.rootDoc.type === DocumentType.RTF ? StrCast(Doc.UserDoc().userColor) : "rgba(0,0,0,0.4)"), + background: SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.userColor || (this.rootDoc.type === DocumentType.RTF ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"), pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : undefined, }}> <EditableView ref={this._titleRef} - contents={this.ShowTitle.split(";").map(field => field + ":" + (this.dataDoc || this.props.Document)[field]?.toString()).join(" ")} - display={"block"} fontSize={10} - GetValue={() => ""} - SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc || this.props.Document)[this.ShowTitle] = value) ? true : true)} + contents={this.ShowTitle === "title" ? StrCast((this.dataDoc || this.props.Document).title) : this.ShowTitle.split(";").map(field => field + ":" + (this.dataDoc || this.props.Document)[field]?.toString()).join(" ")} + display={"block"} + fontSize={10} + GetValue={() => Field.toString((this.dataDoc || this.props.Document)[this.ShowTitle.split(";")[0]] as any as Field)} + SetValue={undoBatch((value: string) => { + this.ShowTitle.includes("Date") ? true : (Doc.GetProto(this.dataDoc || this.props.Document)[this.ShowTitle] = value) ? true : true; + })} /> </div>); return !this.ShowTitle && !showCaption ? @@ -1131,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 0d29e43b8..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. @@ -29,6 +30,7 @@ export interface FieldViewProps { dropAction: dropActionType; backgroundHalo?: () => boolean; docFilters: () => string[]; + docRangeFilters: () => string[]; searchFilterDocs: () => Doc[]; isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; @@ -46,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; @@ -133,7 +135,7 @@ export class FieldView extends React.Component<FieldViewProps> { // ); } else if (field instanceof List) { - return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : "[]"} </div>; + return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : ""} </div>; } // bcz: this belongs here, but it doesn't render well so taking it out for now else if (field instanceof WebField) { diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 067477dcf..6f01e5916 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -48,7 +48,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc const keys = new Set<string>(noviceFields); this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); - return Array.from(keys.keys()).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("_") && !key.startsWith("acl")) || noviceFields.includes(key)).sort(); + return Array.from(keys.keys()).filter(key => key[0]).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("_")) || noviceFields.includes(key) || !Doc.UserDoc().noviceMode).sort(); } gatherFieldValues(dashboard: Doc, facetKey: string) { @@ -94,15 +94,15 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc const docFilter = Cast(targetDoc._docFilters, listSpec("string")); if (docFilter) { let index: number; - while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { - docFilter.splice(index, 3); + while ((index = docFilter.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + docFilter.splice(index, 1); } } const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); if (docRangeFilters) { let index: number; - while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { - docRangeFilters.splice(index, 3); + while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + docRangeFilters.splice(index, 1); } } } else { @@ -187,6 +187,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc fieldKey={`${this.props.fieldKey}`} CollectionView={undefined} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.ContainingCollectionDoc} ContainingCollectionView={this.props.ContainingCollectionView} @@ -223,10 +224,10 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []); - for (let i = 0; i < docFilters.length; i += 3) { - const [header, value, state] = docFilters.slice(i, i + 3); - if (header === facetHeader && value === facetValue) { - return state; + for (const filter of docFilters) { + const fields = filter.split(":"); // split into key:value:modifiers + if (fields[0] === facetHeader && fields[1] === facetValue) { + return fields[2]; } } return undefined; diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 6e96513c7..276c66bb1 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -8,11 +8,12 @@ import { FieldView, FieldViewProps } from './FieldView'; import { StrCast, Cast, ScriptCast } from '../../../fields/Types'; import { Utils, setupMoveUpEvents, returnFalse, emptyFunction } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, AclPrivate } from '../../../fields/Doc'; import { ContextMenu } from '../ContextMenu'; import { ScriptField } from '../../../fields/ScriptField'; import { Tooltip } from '@material-ui/core'; import { DragManager } from '../../util/DragManager'; +import { GetEffectiveAcl } from '../../../fields/util'; const FontIconSchema = createSchema({ icon: "string", }); @@ -105,7 +106,7 @@ export class FontIconBadge extends React.Component<FontIconBadgeProps> { render() { if (!(this.props.collection instanceof Doc)) return (null); - const length = DocListCast(this.props.collection.data).length; + const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read return <div className="fontIconBadge-container" style={{ width: 15, height: 15, top: 12 }} ref={this._notifsRef}> <div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }} onPointerDown={this.onPointerDown} > diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index c2662b35b..88dc3b241 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -67,6 +67,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD @observable static _showControls: boolean; @observable uploadIcon = uploadIcons.idle; + @computed get contentScaling() { return this.props.ContentScaling(); } + protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); @@ -74,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 }); } @@ -104,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(); } } @@ -266,7 +265,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD considerGooglePhotosLink = () => { const remoteUrl = this.dataDoc.googlePhotosUrl; return !remoteUrl ? (null) : (<img draggable={false} - style={{ transform: `scale(${this.props.ContentScaling()})`, transformOrigin: "bottom right" }} + style={{ transform: `scale(${this.contentScaling})`, transformOrigin: "bottom right" }} id={"google-photos"} src={"/assets/google_photos.png"} onClick={() => window.open(remoteUrl)} @@ -291,7 +290,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD return ( <img id={"upload-icon"} draggable={false} - style={{ transform: `scale(${1 / this.props.ContentScaling()})`, transformOrigin: "bottom right" }} + style={{ transform: `scale(${1 / this.contentScaling})`, transformOrigin: "bottom right" }} src={`/assets/${this.uploadIcon}`} onClick={async () => { const { dataDoc } = this; @@ -402,18 +401,18 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD (this.props.PanelHeight() - this.props.PanelWidth() * aspect) / 2 : 0; } - screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter / this.props.ContentScaling()); + screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter / this.contentScaling); contentFunc = () => [this.content]; render() { TraceMobx(); return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} style={{ - transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, - width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, - height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, + 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.props.ContentScaling()}px` + borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.contentScaling}px` }} > <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} forceScaling={true} @@ -434,6 +433,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index e531083bf..2e2319447 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -57,6 +57,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { DataDoc: this.props.doc, LibraryPath: [], docFilters: returnEmptyFilter, + docRangeFilters: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 10b6aa02e..ec8c43ab4 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -73,7 +73,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { this._timeout = setTimeout(action(() => { - DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, "add:right")), false); + DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, (doc, where) => this.props.addDocTab(doc, where), false); this._editing = false; }), 300 - (Date.now() - this._lastTap)); } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 89ea57f65..d47942bd9 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -41,7 +41,7 @@ export class LinkDocPreview extends React.Component<Props> { async followDefault() { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; - this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, doc => this.props.addDocTab(doc, "add:right"), false) : null; + this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, (doc, where) => this.props.addDocTab(doc, where), false) : null; } componentWillUnmount() { LinkDocPreview.TargetDoc = undefined; } @@ -64,7 +64,7 @@ export class LinkDocPreview extends React.Component<Props> { this._toolTipText = ""; LinkDocPreview.TargetDoc = this._targetDoc = target; if (anchor !== this._targetDoc && anchor && this._targetDoc) { - this._targetDoc._scrollY = NumCast(anchor?.y); + this._targetDoc._scrollPreviewY = NumCast(anchor?.y); } }); } @@ -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} @@ -104,12 +101,13 @@ export class LinkDocPreview extends React.Component<Props> { pinToPres={returnFalse} dontRegisterView={true} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} 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 64e0eeb2c..42b24f6f6 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -35,7 +35,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum private _scriptValue: string = ""; private _searchString: string = ""; private _initialScale: number = 0; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live. - private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title + private _displayPdfLive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title private _pdfViewer: PDFViewer | undefined; private _searchRef = React.createRef<HTMLInputElement>(); private _keyRef = React.createRef<HTMLInputElement>(); @@ -44,16 +44,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum private _selectReactionDisposer: IReactionDisposer | undefined; @observable private _searching: boolean = false; - @observable private _flyout: boolean = false; @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; @observable private _pageControls = false; 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) { + if (PDFBox.pdfcache.get(pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(pdfUrl.url.href)); + else if (PDFBox.pdfpromise.get(pdfUrl.url.href)) PDFBox.pdfpromise.get(pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf)); + } const backup = "oldPath"; const { Document } = this.props; @@ -93,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)); } @@ -250,10 +255,11 @@ 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} docFilters={this.props.docFilters} searchFilterDocs={this.props.searchFilterDocs} + addDocTab={this.props.addDocTab} focus={this.props.focus} searchFilterDocs={this.props.searchFilterDocs} + docFilters={this.props.docFilters} docRangeFilters={this.props.docRangeFilters} pinToPres={this.props.pinToPres} addDocument={this.addDocument} Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} @@ -264,22 +270,23 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum </div>; } - _pdfjsRequested = false; + static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>(); + static pdfpromise = new Map<string, Pdfjs.PDFPromise<Pdfjs.PDFDocumentProxy>>(); render() { TraceMobx(); - const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0) || this.props.Document._scrollY !== undefined) { - this._everActive = true; + this._displayPdfLive = true; } - if (pdfUrl && this._everActive) { - if (pdfUrl instanceof PdfField && this._pdf) { - return this.renderPdfView; - } - if (!this._pdfjsRequested) { - this._pdfjsRequested = true; - const promise = Pdfjs.getDocument(pdfUrl.url.href).promise; - promise.then(action(pdf => this._pdf = pdf)); + if (this._displayPdfLive) { + if (this._pdf) return this.renderPdfView; + const href = Cast(this.dataDoc[this.props.fieldKey], PdfField, null)?.url.href; + if (href) { + if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(href))); + else { + if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); + PDFBox.pdfpromise.get(href)?.then(action(pdf => PDFBox.pdfcache.set(href, this._pdf = pdf))); + } } } return this.renderTitleBox; diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss index 7bc6c1dfd..de2aee8fa 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/PresBox.scss @@ -22,7 +22,6 @@ $light-background: #ececec; position: relative; height: calc(100% - 67px); width: 100%; - margin-top: 3px; } .presBox-toolbar-dropdown { @@ -57,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 { @@ -129,7 +131,6 @@ $light-background: #ececec; position: relative; display: inline; font-family: Roboto; - color: black; z-index: 100; transition: 0.7s; @@ -412,6 +413,53 @@ $light-background: #ececec; transform: scale(1.05); transition: all 0.4s; } + + .ribbon-frameList { + width: calc(100% - 5px); + height: 50px; + background-color: #ececec; + border: 1px solid #9f9f9f; + grid-template-rows: max-content; + + .frameList-header { + display: grid; + width: 100%; + height: 20px; + background-color: #9f9f9f; + + .frameList-headerButtons { + display: flex; + grid-column: 7; + width: 60px; + justify-self: right; + justify-content: flex-end; + + .headerButton { + cursor: pointer; + position: relative; + border-radius: 100%; + z-index: 300; + width: 15px; + height: 15px; + display: flex; + font-size: 10px; + justify-self: center; + align-self: center; + background-color: rgba(0, 0, 0, 0.5); + color: white; + justify-content: center; + align-items: center; + transition: 0.2s; + margin-right: 3px; + } + + .headerButton:hover { + background-color: rgba(0, 0, 0, 1); + transform: scale(1.15); + } + } + } + } } .selectedList { @@ -463,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; @@ -523,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; @@ -620,7 +670,7 @@ $light-background: #ececec; position: relative; align-self: flex-start; justify-self: flex-start; - width: 80%; + width: 100%; height: 100%; display: flex; align-items: center; @@ -951,6 +1001,7 @@ $light-background: #ececec; transition: all 0.2s; .presPanel-button-text { + cursor: pointer; display: flex; height: 20; width: max-content; @@ -1003,11 +1054,14 @@ $light-background: #ececec; .presPanel-button:hover { background-color: #5a5a5a; + transform: scale(1.2); } .presPanel-button-text:hover { background-color: #5a5a5a; } + + } // .miniPres { diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 7eca09f8c..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,141 +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._gridGap = 5; + this.layoutDoc.presStatus = PresStatus.Edit; + this.layoutDoc._gridGap = 0; + this.layoutDoc._yMargin = 0; this.turnOffEdit(true); - DocListCastAsync((Doc.UserDoc().myPresentations as Doc).data).then(async pres => { - await Promise.all(pres!); - if (!DocListCast((Doc.UserDoc().myPresentations as Doc).data).includes(this.rootDoc)) Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc); - }); + 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 (targetDoc.scrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(targetDoc, currentFrame); - 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; @@ -238,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. @@ -256,64 +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, "inPlace") : this.props.addDocTab(activeItem, "replace:right"); - } 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(() => { - 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; - } } /** @@ -361,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. @@ -370,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; + } } } }); @@ -392,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(); @@ -405,7 +463,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition); - if (duration <= 100) { duration = 2500; } + if (duration < 100) { duration = 2500; } if (NumCast(targetDoc.lastFrame) > 0) { for (var f = 0; f < NumCast(targetDoc.lastFrame); f++) { await timer(duration / NumCast(targetDoc.lastFrame)); @@ -416,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; } } @@ -440,24 +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; } } + @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; }); } @@ -470,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; } }); @@ -481,19 +547,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> /** * The method called to open the presentation as a minimized view - * TODO: Look at old updateMinimize and compare... */ - @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); @@ -503,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); @@ -519,42 +582,21 @@ 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; // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined); this.rootDoc._viewType = viewType; - if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 5; + 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 @@ -568,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) && @@ -601,61 +648,67 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> */ @action sortArray = (): Doc[] => { - const sort: Doc[] = this._selectedArray; - this.childDocs.forEach((doc, i) => { - if (this._selectedArray.includes(doc)) { - sort.push(doc); - } - }); - return sort; + 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); } @@ -663,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); @@ -733,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; @@ -770,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 @@ -799,36 +932,13 @@ 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 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 @@ -837,13 +947,119 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> 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; @@ -854,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"> @@ -872,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> @@ -890,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={() => 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={() => 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={() => 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={() => targetDoc.presEffect = 'None'}>None</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Fade'}>Fade In</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Flip'}>Flip</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Rotate'}>Rotate</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Bounce'}>Bounce</div> - <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => 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" }}> @@ -939,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={() => 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={() => 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={() => 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={() => 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={() => 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> @@ -976,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; } }); } @@ -1001,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" @@ -1015,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" @@ -1024,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; @@ -1038,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 }}> @@ -1077,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> */} @@ -1093,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> @@ -1133,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> @@ -1184,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"); } @@ -1234,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> @@ -1245,51 +1509,31 @@ 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)]); const currentFrame = Cast(tagDoc._currentFrame, "number", null); if (currentFrame === undefined) { tagDoc._currentFrame = 0; - CollectionFreeFormDocumentView.setupScroll(tagDoc, 0); - CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0); + // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } - CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame); + // if (tagDoc.editScrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame); CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc); tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1); tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame)); - // if (curDoc.zoomProgressivize) { - // const resize = document.getElementById('resizable'); - // if (resize) { - // resize.style.width = this.checkList(tagDoc, curDoc["viewfinder-width-indexed"]) + 'px'; - // resize.style.height = this.checkList(tagDoc, curDoc["viewfinder-height-indexed"]) + 'px'; - // resize.style.top = this.checkList(tagDoc, curDoc["viewfinder-top-indexed"]) + 'px'; - // resize.style.left = this.checkList(tagDoc, curDoc["viewfinder-left-indexed"]) + 'px'; - // } - // } } - @undoBatch @action prevKeyframe = (tagDoc: Doc, actItem: Doc): void => { const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); const currentFrame = Cast(tagDoc._currentFrame, "number", null); if (currentFrame === undefined) { tagDoc._currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice()); tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1); - // if (actItem.zoomProgressivize) { - // const resize = document.getElementById('resizable'); - // if (resize) { - // resize.style.width = this.checkList(tagDoc, actItem["viewfinder-width-indexed"]) + 'px'; - // resize.style.height = this.checkList(tagDoc, actItem["viewfinder-height-indexed"]) + 'px'; - // resize.style.top = this.checkList(tagDoc, actItem["viewfinder-top-indexed"]) + 'px'; - // resize.style.left = this.checkList(tagDoc, actItem["viewfinder-left-indexed"]) + 'px'; - // } - // } } /** @@ -1328,11 +1572,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> return ( <div> <div className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> - <div className="ribbon-box"> + {/* <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 ? "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-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 ? 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> @@ -1346,23 +1590,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </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> */} + <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 ? 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" style={{ display: activeItem.zoomProgressivize || activeItem.scrollProgressivize || activeItem.presProgressivize || activeItem.textProgressivize ? "grid" : "none" }}> + </div> */} + <div className="ribbon-final-box"> Frames <div className="ribbon-doubleButton"> <div className="ribbon-frameSelector"> <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> @@ -1372,7 +1616,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> <Tooltip title={<><div className="dash-tooltip">{"Last frame"}</div></>}><div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div></Tooltip> </div> - <div className="ribbon-toggle" style={{ height: 20, backgroundColor: "#AEDDF8" }} onClick={() => console.log(" TODO: play frames")}>Play</div> + <div className="ribbon-frameList"> + {this.frameListHeader} + {this.frameList} + </div> + <div className="ribbon-toggle" style={{ height: 20, backgroundColor: PresColor.LightBlue }} onClick={() => console.log(" TODO: play frames")}>Play</div> </div> </div> </div> @@ -1421,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; @@ -1474,7 +1722,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> activeItem.scrollProgressivize = !activeItem.scrollProgressivize; const targetDoc: Doc = this.targetDoc; targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize; - CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame)); + // CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame)); if (targetDoc.editScrollProgressivize) { targetDoc.editScrollProgressivize = false; targetDoc._currentFrame = 0; @@ -1526,7 +1774,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> docs.forEach((doc, i) => CollectionFreeFormDocumentView.setupKeyframes([doc], i, true)); targetDoc.lastFrame = targetDoc.lastFrame ? NumCast(targetDoc.lastFrame) : docs.length - 1; } else { - targetDoc.editProgressivize = false; + // targetDoc.editProgressivize = false; activeItem.presProgressivize = false; targetDoc.presProgressivize = false; targetDoc._currentFrame = 0; @@ -1588,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> @@ -1597,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); @@ -1610,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); @@ -1670,29 +1916,43 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get toolbar() { const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Presentation Panel" : "Open Presentation Panel"; - return ( - <div id="toolbarContainer" className={'presBox-toolbar'} style={{ display: this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }}> - <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> + 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'}> + {/* <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> - <div className="toolbar-divider" /> + </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> - <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={"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> ); } @@ -1704,29 +1964,30 @@ 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.Carousel}>Slides</option> - </select> - <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length > 0 ? 1 : 0.3 }}> + <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>3D Carousel</option> + </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> - <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} - </div> + </div>} </span> {this.playButtons} </div> @@ -1734,6 +1995,72 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> ); } + @action + getList = (list: any): List<number> => { + const x: List<number> = list; + return x; + } + + @action + updateList = (list: any): List<number> => { + const targetDoc: Doc = this.targetDoc; + const x: List<number> = list; + x.length + 1; + x[x.length - 1] = NumCast(targetDoc._scrollY); + return x; + } + + @action + newFrame = () => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const type: string = StrCast(targetDoc.type); + if (!activeItem.frameList) activeItem.frameList = new List<number>(); + switch (type) { + case (DocumentType.PDF || DocumentType.RTF || DocumentType.WEB): + this.updateList(activeItem.frameList); + break; + case DocumentType.COL: + break; + default: + break; + } + } + + @computed get frameListHeader() { + return (<div className="frameList-header"> + 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()} /> + </div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Edit in collection"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); console.log('New frame'); }}> + <FontAwesomeIcon icon={"edit"} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + </div> + </div>); + } + + @computed get frameList() { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const frameList: List<number> = this.getList(activeItem.frameList); + if (frameList) { + const frameItems = frameList.map((value) => + <div className="framList-item"> + + </div> + ); + return ( + + <div className="frameList-container"> + {frameItems} + </div> + ); + } else return (null); + + } + @computed get playButtonFrames() { const targetDoc: Doc = this.targetDoc; return ( @@ -1748,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>); @@ -1770,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(); } @@ -1780,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}>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} @@ -1825,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 51506970d..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} @@ -383,6 +416,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 3b9c3359e..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"; @@ -14,7 +13,7 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; -import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue, OmitKeys } from "../../../Utils"; +import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue, OmitKeys, smoothScroll } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; @@ -33,6 +32,7 @@ import "../pdf/PDFViewer.scss"; import React = require("react"); import { Tooltip } from '@material-ui/core'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -43,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; @@ -51,46 +52,84 @@ 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(() => ({ y: this.layoutDoc._scrollY, x: this.layoutDoc._scrollX }), - ({ x, y }) => this.updateScroll(x, y), + this._scrollReactionDisposer = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }), + ({ scrollY, scrollX }) => { + const delay = this._outerRef.current ? 0 : 250; // wait for mainCont and try again to scroll + const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); + const duration = durationStr ? Number(durationStr[1]) : 1000; + if (scrollY !== undefined) { + 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) { + 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) { @@ -104,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() { @@ -138,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) { @@ -158,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; @@ -284,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); @@ -375,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" }); } @@ -403,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; } @@ -455,9 +492,9 @@ 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} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + <Annotation {...this.props} showInfo={emptyFunction} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) } </div>; } @@ -499,6 +536,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum e.stopPropagation(); const targetDoc = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.Document.title, 0, 0, 125, 125); + FormattedTextBox.SelectOnLoad = targetDoc[Id]; const annotationDoc = this.highlight("rgba(173, 216, 230, 0.35)"); // hyperlink color if (annotationDoc) { DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { @@ -506,6 +544,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum if (!e.aborted && e.annoDragData && !e.annoDragData.linkDocument) { e.annoDragData.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); annotationDoc.isLinkButton = true; + annotationDoc.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.Document; } } }); @@ -527,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; } @@ -545,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); @@ -604,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}> @@ -628,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} @@ -670,6 +715,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum ScreenToLocalTransform={this.scrollXf} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> </CollectionFreeFormView> diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index ffa6e904a..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 @@ -246,6 +246,7 @@ export class DashDocView extends React.Component<IDashDocView> { bringToFront={emptyFunction} dontRegisterView={false} docFilters={this.props.tbox?.props.docFilters || returnEmptyFilter} + docRangeFilters={this.props.tbox?.props.docRangeFilters || returnEmptyFilter} searchFilterDocs={this.props.tbox?.props.searchFilterDocs || returnEmptyDoclist} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index d1109b388..b75cc230f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -12,7 +12,6 @@ .formattedTextBox-cont { touch-action: none; - cursor: text; background: inherit; padding: 0; border-width: 0px; @@ -23,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; @@ -38,6 +36,7 @@ } } +.formattedTextBox-outer-selected, .formattedTextBox-outer { position: relative; overflow: auto; @@ -45,6 +44,9 @@ width: 100%; height: unset; } +.formattedTextBox-outer-selected { + cursor: text; +} .formattedTextBox-sidebar-handle { position: absolute; @@ -106,6 +108,15 @@ border-width: 1px; } } +.formattedTextBox-inner-rounded-selected, +.formattedTextBox-inner-selected { + .ProseMirror { + padding:10px; + } + .ProseMirror:hover { + background: unset; + } +} // .menuicon { // display: inline-block; @@ -332,15 +343,6 @@ footnote::after { .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } } -.formattedTextBox-inner-rounded-selected, -.formattedTextBox-inner-selected { - .ProseMirror { - padding:10px; - } - .ProseMirror:hover { - background: unset; - } -} @media only screen and (max-width: 1000px) { @import "../../globalCssVariables"; @@ -357,7 +359,6 @@ footnote::after { .formattedTextBox-cont { touch-action: none; - cursor: text; background: inherit; padding: 0; border-width: 0px; @@ -383,6 +384,7 @@ footnote::after { } } + .formattedTextBox-outer-selected, .formattedTextBox-outer { position: relative; overflow: auto; @@ -399,7 +401,7 @@ footnote::after { height: 35px; background: lightgray; border-radius: 20px; - cursor:grabbing; + cursor: grabbing; } .formattedTextBox-sidebar, diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 1e6477f43..fe38939c5 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { isEqual } from "lodash"; +import { isEqual, max } from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap, selectAll } from "prosemirror-commands"; @@ -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 } 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"; @@ -22,8 +22,8 @@ import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { makeInterface } from "../../../../fields/Schema"; import { Cast, DateCast, NumCast, StrCast, ScriptCast, BoolCast } from "../../../../fields/Types"; -import { TraceMobx, OVERRIDE_acl, GetEffectiveAcl } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents, OmitKeys } from '../../../../Utils'; +import { TraceMobx, GetEffectiveAcl } from '../../../../fields/util'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents, OmitKeys, smoothScroll } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -58,6 +58,9 @@ import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } from './FormattedTextBoxComment'; import React = require("react"); import { DocumentManager } from '../../../util/DocumentManager'; +import { CollectionStackingView } from '../../collections/CollectionStackingView'; +import { CollectionViewType } from '../../collections/CollectionView'; +import { SnappingManager } from '../../../util/SnappingManager'; export interface FormattedTextBoxProps { makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text @@ -286,10 +289,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } else { - - const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!); - json.selection = state.toJSON().selection; - this._editorView.updateState(EditorState.fromJSON(this.config, json)); + const jsonstring = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!; + if (jsonstring) { + const json = JSON.parse(jsonstring); + json.selection = state.toJSON().selection; + this._editorView.updateState(EditorState.fromJSON(this.config, json)); + } } } } @@ -557,11 +562,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, - () => (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%")); + () => setTimeout(action(() => { + const prevWidth = this.sidebarWidth(); + this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%"; + this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + })), false); } sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const bounds = this.CurrentDiv.getBoundingClientRect(); this.layoutDoc._sidebarWidthPercent = "" + 100 * Math.max(0, (1 - (e.clientX - bounds.left) / bounds.width)) + "%"; + this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== "0%"; return false; } @undoBatch @@ -606,7 +616,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ description: `${this.layoutDoc._showSidebar ? "Hide" : "Show"} Sidebar`, event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); !Doc.UserDoc().noviceMode && uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" }); @@ -654,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; } @@ -796,13 +805,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp tr = tr.addMark(pos, pos + node.nodeSize, link); } }); - OVERRIDE_acl(true); + 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)); - OVERRIDE_acl(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)); @@ -821,8 +835,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp () => this.props.makeLink?.(), (linkDoc: Opt<Doc>) => { if (linkDoc) { - const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-"; - const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : ""; + const a1 = Cast(linkDoc.anchor1, Doc, null); + const a2 = Cast(linkDoc.anchor2, Doc, null); + const otherAnchor = Doc.AreProtosEqual(a1, this.rootDoc) ? a2 : a1; + const anchor2Title = StrCast(otherAnchor.title, "-untitled-"); + const anchor2Id = otherAnchor?.[Id] || ""; this.makeLinkToSelection(linkDoc[Id], anchor2Title, "add:right", anchor2Id); } }, @@ -873,7 +890,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ({ width, autoHeight }) => width !== undefined && setTimeout(() => this.tryUpdateHeight(), 0) ); this._disposers.height = reaction( - () => NumCast(this.layoutDoc._height), + () => Cast(this.layoutDoc._height, "number", null), action(height => { if (height !== undefined && height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) { this.layoutDoc._delayAutoHeight = height; @@ -906,27 +923,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp async (scrollToLinkID) => { const findLinkFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; - let offset = 0; + let hadStart = start !== 0; frag.forEach((node, index) => { const examinedNode = findLinkNode(node, editor); - if (examinedNode?.textContent) { - nodes.push(examinedNode); - offset = index; + if (examinedNode?.node.textContent) { + nodes.push(examinedNode.node); + !hadStart && (start = index + examinedNode.start); + hadStart = true; } }); - return { frag: Fragment.fromArray(nodes), start: start + offset }; + return { frag: Fragment.fromArray(nodes), start }; }; const findLinkNode = (node: Node, editor: EditorView) => { if (!node.isText) { const content = findLinkFrag(node.content, editor); - return node.copy(content.frag); + return { node: node.copy(content.frag), start: content.start }; } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor); - return linkIndex !== -1 && marks[linkIndex].attrs.allLinks.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined; + return linkIndex !== -1 && marks[linkIndex].attrs.allLinks.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined; }; - const start = 0; + let start = 0; if (this._editorView && scrollToLinkID) { const editor = this._editorView; const ret = findLinkFrag(editor.state.doc.content, editor); @@ -948,7 +966,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp { fireImmediately: true } ); this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop), - pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true } + pos => this._scrollRef.current?.scrollTo({ top: pos }), { fireImmediately: true } + ); + this._disposers.scrollY = reaction(() => Cast(this.layoutDoc._scrollY, "number", null), + scrollY => { + if (scrollY !== undefined) { + const delay = this._scrollRef.current ? 0 : 250; // wait for mainCont and try again to scroll + const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); + const duration = durationStr ? Number(durationStr[1]) : 1000; + setTimeout(() => this._scrollRef.current && smoothScroll(duration, this._scrollRef.current, Math.abs(scrollY || 0)), delay); + setTimeout(() => { this.Document._scrollTop = scrollY; this.Document._scrollY = undefined; }, duration + delay); + } + }, { fireImmediately: true } ); setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight))); @@ -1114,6 +1143,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + private isActiveTab(el: Element | null | undefined) { + while (el && el !== document.body) { + if (getComputedStyle(el).display === "none") return false; + el = el.parentNode as any; + } + return true; + } + private setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null); const rtfField = Cast((!curText?.Text && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField); @@ -1152,7 +1189,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad; - if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad) { + if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { FormattedTextBox.SelectOnLoad = ""; this.props.select(false); if (FormattedTextBox.SelectOnLoadChar && this._editorView) { @@ -1160,7 +1197,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; - this._editorView.dispatch(this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar).setStoredMarks(storedMarks)); + const tr = this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size).setStoredMarks(storedMarks); + this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); FormattedTextBox.SelectOnLoadChar = ""; } else if (curText?.Text) { selectAll(this._editorView!.state, this._editorView?.dispatch); @@ -1301,6 +1339,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) { y = Math.min(bounds.bottom, window.innerHeight - RichTextMenu.Instance.height); } + this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); setTimeout(() => window.document.activeElement === this.ProseRef?.children[0] && RichTextMenu.Instance.jumpTo(x, y), 250); } } @@ -1408,7 +1447,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const self = this; return new Plugin({ view(newView) { - self.props.isSelected(true) && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); + runInAction(() => self.props.isSelected(true) && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); return self.menuPlugin = new RichTextMenuPlugin({ editorProps: this.props }); } }); @@ -1420,23 +1459,20 @@ 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; public static HadSelection: boolean = false; onBlur = (e: any) => { + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); FormattedTextBox.HadSelection = window.getSelection()?.toString() !== ""; this.endUndoTypingBatch(); this.doLinkOnDeselect(); FormattedTextBox.LiveTextUndo?.end(); FormattedTextBox.LiveTextUndo = undefined; - // move the richtextmenu offscreen - //if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } _lastTimedMark: Mark | undefined = undefined; @@ -1509,38 +1545,92 @@ 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); - this.rootDoc._height = newHeight + Math.max(0, outer); - this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; + this.rootDoc[this.fieldKey + "-height"] = newHeight + Math.max(0, outer); } catch (e) { console.log("Error in tryUpdateHeight"); } } - } + } //else this.rootDoc[this.fieldKey + "-height"] = 0; + } + + @computed get audioHandle() { + return !this.layoutDoc._showAudio ? (null) : + <div className="formattedTextBox-dictation" onClick={action(e => this._recording = !this._recording)} > + <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? "red" : "blue", transitionDelay: "0.6s", opacity: this._recording ? 1 : 0.25, }} icon={"microphone"} size="sm" /> + </div>; + } + + @computed get sidebarHandle() { + const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.author).length; + return !this.props.isSelected() && !(annotated && !this.sidebarWidth()) ? (null) : + <div className="formattedTextBox-sidebar-handle" + style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, background: annotated ? "lightBlue" : undefined }} + onPointerDown={this.sidebarDown} + />; + } + + 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, + yMargin: 0, + chromeStatus: "enabled", + scaleField: this.annotationKey + "-scale", + annotationsKey: this.annotationKey, + isAnnotationOverlay: true, + fitToBox: fitToBox, + focus: this.props.focus, + isSelected: this.props.isSelected, + select: emptyFunction, + active: this.annotationsActive, + ContentScaling: this.sidebarContentScaling, + whenActiveChanged: this.whenActiveChanged, + removeDocument: this.removeDocument, + moveDocument: this.moveDocument, + addDocument: this.addDocument, + CollectionView: undefined, + ScreenToLocalTransform: this.sidebarScreenToLocal, + renderDepth: this.props.renderDepth + 1, + ContainingCollectionDoc: this.props.ContainingCollectionDoc, + }; + return !this.layoutDoc._showSidebar || this.sidebarWidthPercent === "0%" ? (null) : + <div className={"formattedTextBox-sidebar" + (Doc.GetSelectedTool() !== InkTool.None ? "-inking" : "")} + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + {this.layoutDoc.sidebarViewType === CollectionViewType.Freeform ? <CollectionFreeFormView {...collectionProps} /> : <CollectionStackingView {...collectionProps} />} + </div>; } @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); - @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } + 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(); const selected = this.props.isSelected(); const active = this.active(); const scale = this.props.hideOnLeave ? 1 : this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc._isBackground; - selected && setTimeout(() => this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props)); // need to make sure that we update a text box that is selected after updating the one that was deselected - if (!selected && FormattedTextBoxComment.textBox === this) { FormattedTextBoxComment.Hide(); } + const interactive = (Doc.GetSelectedTool() === InkTool.None || SnappingManager.GetIsDragging()) && !this.layoutDoc._isBackground; + if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(() => FormattedTextBoxComment.Hide()); const minimal = this.props.ignoreAutoHeight; - const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? -10 : 0; - const selclass = selected && !this.layoutDoc._singleLine ? "-selected" : ""; + const margins = NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0); + const selPad = Math.min(margins, 10); + 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} @@ -1575,45 +1665,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp })} onDoubleClick={this.onDoubleClick} > - <div className={`formattedTextBox-outer`} ref={this._scrollRef} - style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !active ? "none" : undefined }} + <div className={`formattedTextBox-outer${selclass}`} ref={this._scrollRef} + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !active && !SnappingManager.GetIsDragging() ? "none" : undefined }} onScroll={this.onscrolled} onDrop={this.ondrop} > <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget} style={{ overflow: this.layoutDoc._singleLine ? "hidden" : undefined, - padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : - `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`, - pointerEvents: !active ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : undefined) : undefined + padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${padding}px`, + pointerEvents: !active && !SnappingManager.GetIsDragging() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : undefined) : undefined }} /> </div> - {!this.layoutDoc._showSidebar || this.sidebarWidthPercent === "0%" ? (null) : - <div className={"formattedTextBox-sidebar" + (Doc.GetSelectedTool() !== InkTool.None ? "-inking" : "")} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - PanelHeight={active ? () => 1000 : this.props.PanelHeight} - PanelWidth={this.sidebarWidth} - scaleField={this.annotationKey + "-scale"} - annotationsKey={this.annotationKey} - isAnnotationOverlay={true} - focus={this.props.focus} - isSelected={this.props.isSelected} - select={emptyFunction} - active={this.annotationsActive} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} /> - </div>} - {selected ? <div className="formattedTextBox-sidebar-handle" style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 5px))` }} onPointerDown={this.sidebarDown} /> : (null)} - {!this.layoutDoc._showAudio ? (null) : - <div className="formattedTextBox-dictation" onClick={action(e => this._recording = !this._recording)} > - <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? "red" : "blue", transitionDelay: "0.6s", opacity: this._recording ? 1 : 0.25, }} icon={"microphone"} size="sm" /> - </div>} + {this.sidebarCollection} + {this.sidebarHandle} + {this.audioHandle} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 6a403cb17..81afba4d7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -13,11 +13,12 @@ .FormattedTextBoxComment { background-color: white; border: 8px solid white; + //width: 200px; //display: flex; .FormattedTextBoxComment-info { - margin-bottom: 9px; + margin-bottom: 37px; .FormattedTextBoxComment-title { padding-right: 4px; @@ -28,9 +29,6 @@ font-style: italic; color: rgb(95, 97, 102); font-size: 10px; - padding-bottom: 4px; - margin-bottom: 5px; - } } @@ -61,7 +59,7 @@ } .FormattedTextBoxComment-preview-wrapper { - width: 170px; + //width: 170px; height: 170px; overflow: hidden; //padding-top: 5px; diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 14bbee1ac..3e5a40084 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -6,7 +6,7 @@ import { EditorState, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; import wiki from "wikijs"; -import { Doc, DocCastAsync, Opt } from "../../../../fields/Doc"; +import { Doc, DocCastAsync, Opt, DocListCast } from "../../../../fields/Doc"; import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; @@ -15,9 +15,9 @@ import { DocumentType } from "../../../documents/DocumentTypes"; import { LinkManager } from "../../../util/LinkManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; -import { LinkMenuItem } from "../../linking/LinkMenuItem"; import { ContentFittingDocumentView } from "../ContentFittingDocumentView"; import { DocumentLinksButton } from "../DocumentLinksButton"; +import { DocumentView } from "../DocumentView"; import { LinkDocPreview } from "../LinkDocPreview"; import { FormattedTextBox } from "./FormattedTextBox"; import './FormattedTextBoxComment.scss'; @@ -88,7 +88,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltipInput.type = "checkbox"; FormattedTextBoxComment.tooltip = document.createElement("div"); FormattedTextBoxComment.tooltipText = document.createElement("div"); - FormattedTextBoxComment.tooltipText.style.width = "100%"; + //FormattedTextBoxComment.tooltipText.style.width = "100%"; FormattedTextBoxComment.tooltipText.style.height = "100%"; FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis"; FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); @@ -96,7 +96,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; FormattedTextBoxComment.tooltip.style.maxWidth = "400px"; FormattedTextBoxComment.tooltip.style.maxHeight = "235px"; - FormattedTextBoxComment.tooltip.style.width = "100%"; + //FormattedTextBoxComment.tooltip.style.width = "100%"; FormattedTextBoxComment.tooltip.style.height = "100%"; FormattedTextBoxComment.tooltip.style.overflow = "hidden"; FormattedTextBoxComment.tooltip.style.display = "none"; @@ -117,7 +117,7 @@ export class FormattedTextBoxComment { textBox.props.addDocTab(linkDoc, e.ctrlKey ? "add" : "add:right"); } else { const target = LinkManager.getOppositeAnchor(linkDoc, textBox.dataDoc); - target && LinkMenuItem.followDefault(linkDoc, textBox.dataDoc, target, textBox.props.addDocTab); + target && DocumentView.followLinkClick(linkDoc, textBox.dataDoc, textBox.props, e.shiftKey, e.altKey); } } } @@ -234,6 +234,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.removeChild(FormattedTextBoxComment.tooltipText); } catch (e) { } FormattedTextBoxComment.tooltipText = document.createElement("div"); + FormattedTextBoxComment.tooltipText.className = "FormattedTextBoxComment-toolTipText"; FormattedTextBoxComment.tooltipText.style.width = "100%"; FormattedTextBoxComment.tooltipText.style.height = "100%"; FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis"; @@ -255,11 +256,11 @@ export class FormattedTextBoxComment { docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => { if (linkDoc instanceof Doc) { (FormattedTextBoxComment.tooltipText as any).href = href; - FormattedTextBoxComment.linkDoc = linkDoc; + FormattedTextBoxComment.linkDoc = DocListCast(textBox.props.Document.links).find(link => link.anchor1 === textBox.props.Document || link.anchor2 === textBox.props.Document ? link : undefined) || linkDoc; const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; if (anchor !== target && anchor && target) { - target._scrollY = NumCast(anchor?.y); + target._scrollPreviewY = NumCast(anchor?.y); } if (target?.author) { FormattedTextBoxComment.showCommentbox("", view, nbef); @@ -308,6 +309,7 @@ export class FormattedTextBoxComment { pinToPres={returnFalse} dontRegisterView={true} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} @@ -318,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>; @@ -328,7 +330,7 @@ export class FormattedTextBoxComment { ReactDOM.render(docPreview, FormattedTextBoxComment.tooltipText); - FormattedTextBoxComment.tooltip.style.width = "100%"; + //FormattedTextBoxComment.tooltip.style.width = "100%"; FormattedTextBoxComment.tooltip.style.height = "100%"; } } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 68239a8f1..cf9b03308 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -31,8 +31,9 @@ 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>(); - public view?: EditorView; + @observable public view?: EditorView; public editorProps: FieldViewProps & FormattedTextBoxProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); @@ -153,11 +154,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { public delayHide = () => this._delayHide = true; @action - public updateMenu(view: EditorView, lastState: EditorState | undefined, props: any) { - if (!view || !(view as any).TextView?.props.isSelected(true) || !view.hasFocus()) { + 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; + } props && (this.editorProps = props); // Don't do anything if the document/selection didn't change @@ -366,7 +370,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { ); } - createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => {}): JSX.Element { + createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => void): JSX.Element { const items = options.map(({ title, label, hidden, style }) => { if (hidden) { return <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>; @@ -378,19 +382,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { function onChange(e: React.ChangeEvent<HTMLSelectElement>) { e.stopPropagation(); e.preventDefault(); - self.TextView.endUndoTypingBatch(); - options.forEach(({ label, mark, command }) => { - if (e.target.value === label && mark) { - if (!self.TextView.props.isSelected(true)) { - switch (mark.type) { - case schema.marks.pFontFamily: setter(Doc.UserDoc().fontFamily = mark.attrs.family); break; - case schema.marks.pFontSize: setter(Doc.UserDoc().fontSize = mark.attrs.fontSize.toString() + "px"); break; + self.TextView?.endUndoTypingBatch(); + UndoManager.RunInBatch(() => { + options.forEach(({ label, mark, command }) => { + if (e.target.value === label && mark) { + if (!self.TextView?.props.isSelected(true)) { + switch (mark.type) { + case schema.marks.pFontFamily: setter(Doc.UserDoc().fontFamily = mark.attrs.family); break; + case schema.marks.pFontSize: setter(Doc.UserDoc().fontSize = mark.attrs.fontSize.toString() + "px"); break; + } } + else self.view && mark && command(mark, self.view); } - else UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown"); - } - }); + }); + }, "text mark dropdown"); } + return <Tooltip key={key} title={<div className="dash-tooltip">{key}</div>} placement="bottom"> <select onChange={onChange} value={activeOption}>{items}</select> </Tooltip>; @@ -424,9 +431,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } changeFontSize = (mark: Mark, view: EditorView) => { - if ((this.view?.state.selection.$from.pos || 0) < 2) { - this.TextView.layoutDoc._fontSize = mark.attrs.fontSize === Number(mark.attrs.fontSize) ? `${mark.attrs.fontSize}pt` : mark.attrs.fontSize; - } const fmark = view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }); this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); view.focus(); @@ -434,9 +438,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } changeFontFamily = (mark: Mark, view: EditorView) => { - if ((this.view?.state.selection.$from.pos || 0) < 2) { - this.TextView.layoutDoc._fontFamily = mark.attrs.family; - } const fmark = view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }); this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); view.focus(); @@ -795,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"); } @@ -810,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> @@ -984,8 +985,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { {this.collapsed ? this.getDragger() : (null)} <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> <div className="richTextMenu-divider" key="divider 3" />, - {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => this.activeFontSize = val)), - this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => this.activeFontFamily = val)), + {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { + this.activeFontSize = val; + SelectionManager.SelectedDocuments().map(dv => dv.props.Document._fontSize = val); + })), + this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { + this.activeFontFamily = val; + SelectionManager.SelectedDocuments().map(dv => dv.props.Document._fontFamily = val); + })), <div className="richTextMenu-divider" key="divider 4" />, this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 921c0e128..99334b6db 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -3,7 +3,8 @@ import { NodeSelection, TextSelection } from "prosemirror-state"; import { DataSym, Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { ComputedField } from "../../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { NumCast, StrCast } from "../../../../fields/Types"; +import { normalizeEmail } from "../../../../fields/util"; import { returnFalse, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -11,7 +12,6 @@ import { FormattedTextBox } from "./FormattedTextBox"; import { wrappingInputRule } from "./prosemirrorPatches"; import { RichTextMenu } from "./RichTextMenu"; import { schema } from "./schema_rts"; -import { List } from "../../../../fields/List"; export class RichTextRules { public Document: Doc; @@ -267,11 +267,11 @@ export class RichTextRules { // [[fieldKey]] => show field // [[fieldKey:Doc]] => show field of doc new InputRule( - new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\.\? \-0-9]+)?\]\]$/), + new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const rawdocid = match[3]?.substring(1); - const docid = rawdocid ? (!rawdocid.includes("@") ? Doc.CurrentUserEmail + "@" + rawdocid : rawdocid).replace(".", "_") : undefined; + const rawdocid = match[3]; + const docid = rawdocid ? normalizeEmail((!rawdocid.includes("@") ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1))) : undefined; const value = match[2]?.substring(1); if (!fieldKey) { const linkId = Utils.GenerateGuid(); @@ -304,7 +304,7 @@ export class RichTextRules { const fieldKey = match[1] || ""; const fieldParam = match[2]?.replace("…", "...") || ""; const rawdocid = match[3]?.substring(1); - const docid = rawdocid ? (!rawdocid.includes("@") ? Doc.CurrentUserEmail + "@" + rawdocid : rawdocid).replace(".", "_") : undefined; + const docid = rawdocid ? (!rawdocid.includes("@") ? normalizeEmail(Doc.CurrentUserEmail) + "@" + rawdocid : rawdocid) : undefined; if (!fieldKey && !docid) return state.tr; docid && DocServer.GetRefField(docid).then(docx => { if (!(docx instanceof Doc && docx)) { diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index 962085f0d..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 @@ -154,6 +154,7 @@ export class DashDocView { bringToFront={emptyFunction} dontRegisterView={false} docFilters={this._textBox.props.docFilters} + docRangeFilters={this._textBox.props.docRangeFilters} searchFilterDocs={this._textBox.props.searchFilterDocs} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index cc326eb93..8b242854d 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -3,4 +3,5 @@ user-select: none; position: absolute; background-color: rgba(146, 245, 95, 0.467); + transition: opacity 0.5s; }
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index f680a00df..20ea7bfe4 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, WidthSym, Field, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { Cast, FieldValue, BoolCast, NumCast, StrCast, PromiseValue } from "../../../fields/Types"; @@ -15,8 +15,10 @@ interface IAnnotationProps { addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc, unpin?: boolean) => void; focus: (doc: Doc) => void; + select: (isCtrlPressed: boolean) => void; dataDoc: Doc; fieldKey: string; + showInfo: (anno: Opt<Doc>) => void; } @observer @@ -24,20 +26,23 @@ export class Annotation extends React.Component<IAnnotationProps> { render() { return DocListCast(this.props.anno.annotations).map(a => - <RegionAnnotation {...this.props} pinToPres={this.props.pinToPres} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />); + <RegionAnnotation {...this.props} showInfo={this.props.showInfo} select={this.props.select} pinToPres={this.props.pinToPres} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />); } } interface IRegionAnnotationProps { + anno: Doc; x: number; y: number; width: number; height: number; addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc, unpin: boolean) => void; + select: (isCtrlPressed: boolean) => void; document: Doc; dataDoc: Doc; fieldKey: string; + showInfo: (anno: Opt<Doc>) => void; } @observer @@ -66,6 +71,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { this._reactionDisposer && this._reactionDisposer(); } + @undoBatch deleteAnnotation = () => { const annotation = DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]); const group = FieldValue(Cast(this.props.document.group, Doc)); @@ -81,6 +87,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { PDFMenu.Instance.fadeOut(true); } + @undoBatch pinToPres = () => { const group = FieldValue(Cast(this.props.document.group, Doc)); const isPinned = group && Doc.isDocPinned(group) ? true : false; @@ -93,7 +100,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { group.isPushpin = !group.isPushpin; }); - isPushpin = () => BoolCast(Cast(this.props.document.group, Doc, null).isPushpin); + isPushpin = () => BoolCast(Cast(this.props.document.group, Doc, null)?.isPushpin); @action onPointerDown = (e: React.PointerEvent) => { @@ -112,7 +119,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { e.persist(); e.stopPropagation(); PromiseValue(this.props.document.group).then(annoGroup => annoGroup instanceof Doc && - DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation), false, undefined) + DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation), false, undefined, + () => this.props.select(false)) ); } } @@ -128,16 +136,17 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { return false; } + @observable _showInfo = false; render() { - return (<div className="pdfAnnotation" onPointerDown={this.onPointerDown} ref={this._mainCont} + return (<div className="pdfAnnotation" onPointerEnter={action(() => this.props.showInfo(this.props.anno))} onPointerLeave={action(() => this.props.showInfo(undefined))} onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ top: this.props.y, left: this.props.x, width: this.props.width, height: this.props.height, - opacity: this._brushed ? 0.5 : undefined, + opacity: !this._showInfo && this._brushed ? 0.5 : undefined, backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor), - transition: "opacity 0.5s", - }} />); + }} > + </div>); } }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 70b4c417c..0ecb4dba4 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -59,10 +59,22 @@ left: 0px; display: inline-block; width:100%; + pointer-events: all; } .pdfViewerDash-overlay { pointer-events: none; } + + .pdfViewerDash-overlayAnno { + top: -17px; + pointer-events: none; + width: max-content; + height: 20px; + position: absolute; + background: #b8b8b8; + border-radius: 5px; + display: block; + } .pdfViewerDash-annotationLayer { position: absolute; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index b79be2e68..48c7b1762 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -3,14 +3,14 @@ import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Dictionary } from "typescript-collections"; -import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym, Field } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; import { createSchema, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast } from "../../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils, OmitKeys } from "../../../Utils"; @@ -26,7 +26,6 @@ import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionView } from "../collections/CollectionView"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; -import { DocumentDecorations } from "../DocumentDecorations"; import { Annotation } from "./Annotation"; import { PDFMenu } from "./PDFMenu"; import "./PDFViewer.scss"; @@ -35,6 +34,8 @@ import React = require("react"); import { LinkDocPreview } from "../nodes/LinkDocPreview"; import { FormattedTextBoxComment } from "../nodes/formattedText/FormattedTextBoxComment"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { SharingManager } from "../../util/SharingManager"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); const _global = (window /* browser */ || global /* node */) as any; @@ -59,9 +60,10 @@ interface IViewerProps { fieldKey: string; Document: Doc; DataDoc?: Doc; - docFilters: () => string[]; searchFilterDocs: () => Doc[]; ContainingCollectionView: Opt<CollectionView>; + docFilters: () => string[]; + docRangeFilters: () => string[]; PanelWidth: () => number; PanelHeight: () => number; ContentScaling: () => number; @@ -71,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; @@ -100,6 +102,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @observable private _showWaiting = true; @observable private _showCover = false; @observable private _zoomed = 1; + @observable private _overlayAnnoInfo: Opt<Doc>; private _pdfViewer: any; private _styleRule: any; // stylesheet rule for making hyperlinks clickable @@ -119,8 +122,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _viewerIsSetup = false; @computed get allAnnotations() { - return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]). - filter(anno => this._script.run({ this: anno }, console.log, true).result); + return DocUtils.FilterDocs(DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]), this.props.docFilters(), this.props.docRangeFilters(), undefined); } @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } @@ -134,7 +136,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu const coreFilename = pathComponents.pop()!.split(".")[0]; const params: any = { coreFilename, - pageNum: this.Document._curPage || 1, + pageNum: Math.min(this.props.pdf.numPages, Math.max(1, this.Document._curPage || 1)), }; if (pathComponents.length) { params.subtree = `${pathComponents.join("/")}/`; @@ -143,7 +145,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } else { const params: any = { coreFilename: relative.split("/")[relative.split("/").length - 1], - pageNum: this.Document._curPage || 1, + pageNum: Math.min(this.props.pdf.numPages, Math.max(1, this.Document._curPage || 1)), }; this._coverPath = "http://cs.brown.edu/~bcz/face.gif";//href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; } @@ -153,6 +155,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0; const observer = new _global.ResizeObserver(action((entries: any) => this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0))); observer.observe(this._mainCont.current); + this._mainCont.current.addEventListener("scroll", (e) => (e.target as any).scrollLeft = 0); } this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.rootDoc), @@ -176,12 +179,27 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu (scrollY) => { if (scrollY !== undefined) { (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); - if ((this.props.renderDepth === -1 || (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc)) && this._mainCont.current) { - smoothScroll(1000, this._mainCont.current, (this.Document._scrollY || 0)); - } else { - console.log("Waiting for preview"); + if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { + const delay = this._mainCont.current ? 0 : 250; // wait for mainCont and try again to scroll + const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); + const duration = durationStr ? Number(durationStr[1]) : 1000; + setTimeout(() => this._mainCont.current && smoothScroll(duration, this._mainCont.current, Math.abs(scrollY || 0)), delay); + setTimeout(() => { this.Document._scrollTop = scrollY; this.Document._scrollY = undefined; }, duration + delay); + } + } + }, + { fireImmediately: true } + ); + this._disposers.scrollPreviewY = reaction( + () => Cast(this.Document._scrollPreviewY, "number", null), + (scrollY) => { + if (scrollY !== undefined) { + (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); + if (this.props.renderDepth === -1 && scrollY >= 0) { + if (!this._mainCont.current) setTimeout(() => this._mainCont.current && smoothScroll(1000, this._mainCont.current, scrollY || 0)); + else smoothScroll(1000, this._mainCont.current, scrollY || 0); + this.Document._scrollPreviewY = undefined; } - setTimeout(() => this.Document._scrollY = undefined, 1000); } }, { fireImmediately: true } @@ -220,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; @@ -337,6 +355,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu mainAnnoDocProto.y = Math.max(minY, 0); mainAnnoDocProto.x = Math.max(maxX, 0); mainAnnoDocProto.type = DocumentType.PDFANNO; + mainAnnoDocProto.text = this._selectionText; mainAnnoDocProto.annotations = new List<Doc>(annoDocs); } mainAnnoDocProto.title = "Annotation on " + this.Document.title; @@ -384,12 +403,12 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onScroll = (e: React.UIEvent<HTMLElement>) => { if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { - this.Document._scrollY === undefined && (this.layoutDoc._scrollTop = this._mainCont.current!.scrollTop); this.pageDelay && clearTimeout(this.pageDelay); this.pageDelay = setTimeout(() => { + this.Document._scrollY === undefined && this._mainCont.current && (this.layoutDoc._scrollTop = this._mainCont.current.scrollTop); this.pageDelay = undefined; - this._pdfViewer && (this.Document._curPage = this._pdfViewer.currentPageNumber); - }, 250); + //this._pdfViewer && (this.Document._curPage = this._pdfViewer.currentPageNumber); + }, 1000); } } @@ -457,7 +476,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onPointerDown = (e: React.PointerEvent): void => { const hit = document.elementFromPoint(e.clientX, e.clientY); - if (hit && hit.localName === "span" && this.props.isSelected(true)) { // drag selecting text stops propagation + if (hit && hit.localName === "span" && this.annotationsActive(true)) { // drag selecting text stops propagation e.button === 0 && e.stopPropagation(); } // if alt+left click, drag and annotate @@ -527,8 +546,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu if (rect) { const scaleY = this._mainCont.current.offsetHeight / boundingRect.height; const scaleX = this._mainCont.current.offsetWidth / boundingRect.width; - if (rect.width !== this._mainCont.current.clientWidth && - (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) { + if (rect.width !== this._mainCont.current.clientWidth) { const annoBox = document.createElement("div"); annoBox.className = "pdfViewerDash-annotationBox"; // transforms the positions from screen onto the pdf div @@ -554,6 +572,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onSelectEnd = (e: PointerEvent): void => { clearStyleSheetRules(PDFViewer._annotationStyle); + this.props.select(false); this._savedAnnotations.clear(); if (this._marqueeing) { if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { @@ -623,6 +642,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu clipDoc._height = this.marqueeHeight(); clipDoc._scrollTop = this.marqueeY(); const targetDoc = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.Document.title, 0, 0, 100, 100); + FormattedTextBox.SelectOnLoad = targetDoc[Id]; Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]); clipDoc.rootDocument = targetDoc; // DocUtils.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined); @@ -639,6 +659,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu e.annoDragData.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); } annotationDoc.isLinkButton = true; // prevents link button fro showing up --- maybe not a good thing? + annotationDoc.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.Document; e.annoDragData && e.annoDragData.linkDocument && e.annoDragData?.linkDropCallback?.({ linkDocument: e.annoDragData.linkDocument }); } }); @@ -660,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` }} />; @@ -687,18 +708,33 @@ 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} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + <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`} />) } </div>; } + + @computed get overlayInfo() { + return !this._overlayAnnoInfo || this._overlayAnnoInfo.author === Doc.CurrentUserEmail ? (null) : + <div className="pdfViewerDash-overlayAnno" style={{ top: NumCast(this._overlayAnnoInfo.y), left: NumCast(this._overlayAnnoInfo.x) }}> + <div className="pdfViewerDash-overlayAnno" style={{ right: -50, background: SharingManager.Instance.users.find(users => users.user.email === this._overlayAnnoInfo!.author)?.userColor }}> + {this._overlayAnnoInfo.author + " " + Field.toString(this._overlayAnnoInfo.creationDate as Field)} + </div> + </div>; + } + + 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" : ""}`} id="overlay" - style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined, transform: `scale(${this._zoomed})` }}> + return <div className={`pdfViewerDash-overlay${Doc.GetSelectedTool() !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} + 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} @@ -719,6 +755,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} CollectionView={undefined} ScreenToLocalTransform={this.overlayTransform} renderDepth={this.props.renderDepth + 1} @@ -745,17 +783,18 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu contentZoom = () => this._zoomed; render() { TraceMobx(); - return <div className={"pdfViewerDash" + (this.active() ? "-interactive" : "")} ref={this._mainCont} + return <div className={"pdfViewerDash" + (this.annotationsActive() ? "-interactive" : "")} ref={this._mainCont} 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} /> </div >; diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss index 4642caeb2..73a08b6de 100644 --- a/src/client/views/presentationview/PresElementBox.scss +++ b/src/client/views/presentationview/PresElementBox.scss @@ -1,12 +1,13 @@ $light-blue: #AEDDF8; $dark-blue: #5B9FDD; $light-background: #ececec; +$slide-background: #d5dce2; +$slide-active: #5B9FDD; -.presElementBox-item { +.presItem-container { cursor: grab; display: grid; - grid-template-columns: max-content max-content max-content max-content; - background-color: #d5dce2; + grid-template-columns: 20px auto; font-family: Roboto; letter-spacing: normal; position: relative; @@ -14,200 +15,188 @@ $light-background: #ececec; width: 100%; height: 100%; font-weight: 400; - border-radius: 6px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; - transition: all .3s; - padding: 0px; - padding-bottom: 3px; + align-items: center; - .presElementBox-highlight { - position: absolute; - transform: translate(-100px, -4px); - z-index: -1; - width: calc(100% + 200px); - height: calc(100% + 8px); - background-color: $light-blue; + .presItem-number { + margin-top: 3.5px; + font-size: 12px; + font-weight: 700; + text-align: center; + justify-self: center; + align-self: flex-start; + position: relative; + display: inline-block; + overflow: hidden; } - .presElementBox-highlightTop { - position: absolute; - transform: translate(-100px, -4px); - z-index: -1; - width: calc(100% + 200px); - height: calc(50% + 4px); +} + +.presItem-slide { + position: relative; + background-color: #d5dce2; + border-radius: 5px; + height: calc(100% - 7px); + width: calc(100% - 15px); + display: grid; + 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: 11px; + font-family: Roboto; + font-weight: 500; + position: relative; + padding-left: 10px; + padding-right: 10px; + letter-spacing: normal; + width: max-content; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; } - .presElementBox-highlightBottom { - position: absolute; - transform: translate(-100px, 0px); - z-index: -1; - top: 50%; - width: calc(100% + 200px); - height: calc(50% + 4px); + .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; } - .documentView-node { - position: absolute; - z-index: 1; + .presItem-time { + align-self: center; + position: relative; + padding-right: 10px; + top: 1px; + font-size: 10; + font-weight: 300; + font-family: Roboto; + z-index: 300; + letter-spacing: normal; } -} -.presElementBox-item-above { - border-top: black 2px solid; -} + .presItem-embedded { + overflow: hidden; + grid-row: 3; + grid-column: 1/8; + position: relative; + display: flex; + width: auto; + justify-content: center; + margin: auto; + margin-bottom: 2px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } -.presElementBox-item-below { - border-bottom: black 2px solid; -} + .presItem-embeddedMask { + width: 100%; + height: 100%; + position: absolute; + border-radius: 3px; + top: 0; + left: 0; + z-index: 1; + overflow: hidden; + } -.presElementBox-item:hover { - transition: all .1s; - background: #98b7da; - border-radius: 6px; -} -.presElementBox-active { - color: black; - border-radius: 6px; - border: solid 2px $dark-blue; + .presItem-slideButtons { + display: flex; + grid-column: 7; + grid-row: 1/3; + width: 60px; + justify-self: right; + justify-content: flex-end; + + .slideButton { + cursor: pointer; + position: relative; + border-radius: 100%; + z-index: 300; + width: 18px; + height: 18px; + display: flex; + font-size: 12px; + justify-self: center; + align-self: center; + background-color: rgba(0, 0, 0, 0.5); + color: white; + justify-content: center; + align-items: center; + transition: 0.2s; + margin-right: 3px; + } + + .slideButton:hover { + background-color: rgba(0, 0, 0, 1); + transform: scale(1.2); + } + } } -.presElementBox-buttons { - display: grid; - grid-template-rows: 15px; - top: 15px; - left: -20; - position: absolute; - width: 100%; - height: auto; - .presElementBox-interaction { - display: none; - } - .presElementBox-interaction-selected { - color: grey; - background-color: rgba(0, 0, 0, 0); - float: left; - padding: 0px; - width: 20px; - height: 20px; - } +.presItem-slide.active { + box-shadow: 0 0 0px 1.5px $dark-blue; } -.presElementBox-number { - font-size: 12px; - width: 20; - font-weight: 700; - text-align: right; +.presItem-multiDrag { + font-family: Roboto; + font-weight: 600; + color: white; + text-align: center; justify-content: center; align-content: center; - left: -20; + width: 100px; + height: 30px; position: absolute; - display: inline-block; - overflow: hidden; + background-color: $dark-blue; + z-index: 4000; + border-radius: 10px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + line-height: 30px; } -.presElementBox-name { - z-index: 300; - align-self: center; - font-size: 13px; - font-family: Roboto; - font-weight: 500; - position: relative; - top: 1px; - padding-left: 10px; - padding-right: 10px; - letter-spacing: normal; - width: max-content; - text-overflow: ellipsis; - overflow: hidden; - white-space: pre; -} - -.presElementBox-time { - align-self: center; - position: relative; - top: 2px; - padding-right: 10px; - font-size: 10; - font-weight: 300; - font-family: Roboto; - z-index: 300; - letter-spacing: normal; -} - -.presElementBox-embedded { +.presItem-miniSlide { + font-weight: 700; + font-size: 12; grid-column: 1/8; - position: relative; - display: flex; - width: auto; - justify-content: center; - margin: auto; -} - -.presElementBox-embeddedMask { - width: 100%; - height: 100%; - position: absolute; - border-radius: 3px; - top: 0; - left: 0; - z-index: 1; - overflow: hidden; -} - -.presElementBox-closeIcon { - cursor: pointer; - position: absolute; + align-self: center; + justify-self: center; + background-color: #d5dce2; + width: 26px; + text-align: center; + height: 26px; + line-height: 28px; border-radius: 100%; - z-index: 300; - right: 3px; - top: 3px; - width: 20px; - height: 20px; - display: flex; - font-size: 75%; - background-color: black; - color: white; - justify-content: center; - align-items: center; } -.presElementBox-expand { - cursor: pointer; - position: absolute; - border-radius: 100%; - z-index: 300; - right: 26px; - top: 3px; - width: 20px; - height: 20px; - display: flex; - font-size: 75%; - background-color: black; - color: white; - justify-content: center; - align-items: center; +.presItem-miniSlide.active { + box-shadow: 0 0 0px 1.5px $dark-blue; } -.presElementBox-expand-selected { - cursor: pointer; - position: absolute; - border-radius: 100%; - right: 3px; - bottom: 3px; - width: 20px; - height: 20px; - z-index: 300; - display: flex; - background-color: black; - color: white; - justify-content: center; - align-items: center; -}
\ No newline at end of file +// .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 e9ab5911d..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 } 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'; @@ -8,13 +8,11 @@ import { createSchema, makeInterface, listSpec } from '../../../fields/Schema'; import { Cast, NumCast, BoolCast, ScriptCast, StrCast } from "../../../fields/Types"; import { emptyFunction, emptyPath, returnFalse, returnTrue, returnOne, returnZero, numberRange, setupMoveUpEvents } from "../../../Utils"; import { Transform } from "../../util/Transform"; -import { CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from '../DocComponent'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./PresElementBox.scss"; import React = require("react"); -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { PresBox, PresMovement } from "../nodes/PresBox"; import { DocumentType } from "../../documents/DocumentTypes"; import { Tooltip } from "@material-ui/core"; @@ -22,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, @@ -45,6 +44,8 @@ const PresDocument = makeInterface(presSchema, documentSchema); export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDocument>(PresDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); } _heightDisposer: IReactionDisposer | undefined; + + @observable _dragging = false; // these fields are conditionally computed fields on the layout document that take this document as a parameter @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up @@ -71,17 +72,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; } - embedHeight = () => 100; + embedHeight = (): number => 97; // embedWidth = () => this.props.PanelWidth(); // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); - embedWidth = () => this.props.PanelWidth() - 20; + embedWidth = (): number => this.props.PanelWidth() - 30; /** * The function that is responsible for rendering a preview or not for this * presentation element. */ @computed get renderEmbeddedInline() { return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) : - <div className="presElementBox-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}> + <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}> <ContentFittingDocumentView Document={this.targetDoc} DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} @@ -104,12 +105,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc bringToFront={returnFalse} opacity={returnOne} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} ContentScaling={returnOne} /> - <div className="presElementBox-embeddedMask" /> + <div className="presItem-embeddedMask" /> </div>; } @@ -118,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(); @@ -139,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); } } } @@ -156,50 +156,68 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc headerUp = (e: React.PointerEvent<HTMLDivElement>) => { e.stopPropagation(); e.preventDefault(); - DragManager.docsBeingDragged = []; - this._highlightTopRef.current!.style.borderBottom = "0px"; - this._highlightBottomRef.current!.style.borderBottom = "0px"; } - 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 drag = ele; - drag.style.backgroundColor = "#d5dce2"; - drag.style.borderRadius = '5px'; - dragItem.push(drag); - }); + 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); - activeItem.dragging = true; + DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined); + // runInAction(() => this._dragging = true); return true; } return false; } - private _highlightTopRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _highlightBottomRef: React.RefObject<HTMLDivElement> = React.createRef(); - - - onPointerTop = (e: React.PointerEvent<HTMLDivElement>) => { - if (DragManager.docsBeingDragged.length > 1) { - this._highlightTopRef.current!.style.borderTop = "solid 2px #5B9FDD"; - } + onPointerOver = (e: any) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); } - onPointerBottom = (e: React.PointerEvent<HTMLDivElement>) => { - if (DragManager.docsBeingDragged.length > 1) { - this._highlightBottomRef.current!.style.borderBottom = "solid 2px #5B9FDD"; + onPointerMove = (e: PointerEvent) => { + const slide = this._itemRef.current!; + 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; + const halfLine = height / 2; + if (y <= halfLine) { + slide.style.borderTop = "solid 2px #5B9FDD"; + slide.style.borderBottom = "0px"; + } else if (y > halfLine) { + slide.style.borderTop = "0px"; + slide.style.borderBottom = "solid 2px #5B9FDD"; + } + } + document.removeEventListener("pointermove", this.onPointerMove); } - onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => { - if (DragManager.docsBeingDragged.length > 1) { - this._highlightBottomRef.current!.style.borderBottom = "0px"; - this._highlightTopRef.current!.style.borderTop = "0px"; - } + onPointerLeave = (e: any) => { + this._itemRef.current!.style.borderTop = "0px"; + this._itemRef.current!.style.borderBottom = "0px"; + document.removeEventListener("pointermove", this.onPointerMove); } @action @@ -212,86 +230,130 @@ 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; } - render() { - const className = "presElementBox-item" + (PresBox.Instance._selectedArray.includes(this.rootDoc) ? " presElementBox-active" : ""); - return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : ( - <div className={className} key={this.props.Document[Id] + this.indexInPres} + /** + * Method called for updating the view of the currently selected document + * + * @param targetDoc + * @param activeItem + */ + @undoBatch + @action + 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; + const scale = targetDoc._viewScale; + activeItem.presPinViewX = x; + activeItem.presPinViewY = y; + activeItem.presPinViewScale = scale; + } + } + + @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.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} ref={this._itemRef} - style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }} + style={{ backgroundColor: isSelected ? "#AEDDF8" : "rgba(0,0,0,0)", opacity: this._dragging ? 0.3 : 1 }} 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); - PresBox.Instance._eleArray = []; - PresBox.Instance._eleArray.push(this._itemRef.current!); - PresBox.Instance._dragArray = []; - PresBox.Instance._dragArray.push(this._dragRef.current!); - } + 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 => { - console.log('double click to open'); + onDoubleClick={action(e => { this.toggleProperties(); - this.props.focus(this.rootDoc); - PresBox.Instance._eleArray = []; - PresBox.Instance._eleArray.push(this._itemRef.current!); - PresBox.Instance._dragArray = []; - PresBox.Instance._dragArray.push(this._dragRef.current!); - }} + 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="presElementBox-number"> + {miniView ? + <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}> {`${this.indexInPres + 1}.`} </div> - <div ref={this._dragRef} className="presElementBox-name" style={{ maxWidth: (PresBox.Instance.toolbarWidth - 70) }}> + : + <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; - })} + 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="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.transition}</div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.duration}</div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Presentation pin view"}</div></>}><div className="presElementBox-time" style={{ fontWeight: 700, display: this.rootDoc.presPinView && PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>V</div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div - className="presElementBox-closeIcon" - onClick={this.removeItem}> - <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> - </div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"presElementBox-expand" + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> - <FontAwesomeIcon icon={(this.rootDoc.presExpandInlineButton ? "angle-up" : "angle-down")} onPointerDown={e => e.stopPropagation()} /> - </div></Tooltip> - </> - <div ref={this._highlightTopRef} onPointerOver={this.onPointerTop} onPointerLeave={this.onPointerLeave} className="presElementBox-highlightTop" style={{ zIndex: 299, backgroundColor: "rgba(0,0,0,0)" }} /> - <div ref={this._highlightBottomRef} onPointerOver={this.onPointerBottom} onPointerLeave={this.onPointerLeave} className="presElementBox-highlightBottom" style={{ zIndex: 299, backgroundColor: "rgba(0,0,0,0)" }} /> - <div className="presElementBox-highlight" style={{ backgroundColor: PresBox.Instance._selectedArray.includes(this.rootDoc) ? "#AEDDF8" : "rgba(0,0,0,0)" }} /> - {this.renderEmbeddedInline} - </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.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> + <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div + className={"slideButton"} + onClick={this.removeItem}> + <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 >); + } + + render() { + 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 a6ac62ee4..3adeb6133 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -24,6 +24,8 @@ import { ViewBoxBaseComponent } from "../DocComponent"; 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 }); @@ -52,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[] = []; @@ -121,22 +125,24 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const filters: string[] = []; - for (let i = 0; i < initialfilters.length; i = i + 3) { - if (initialfilters[i + 2] !== undefined) { - filters.push(initialfilters[i]); - filters.push(initialfilters[i + 1]); - filters.push(initialfilters[i + 2]); + for (const initFilter of initialfilters) { + const fields = initFilter.split(":"); + if (fields[2] !== undefined) { + filters.push(fields[0]); + filters.push(fields[1]); + filters.push(fields[2]); } } const finalfilters: { [key: string]: string[] } = {}; - for (let i = 0; i < filters.length; i = i + 3) { - if (finalfilters[filters[i]] !== undefined) { - finalfilters[filters[i]].push(filters[i + 1]); + for (let i = 0; i < filters.length; i = i++) { + const fields = filters[i].split(":"); + if (finalfilters[fields[0]] !== undefined) { + finalfilters[fields[0]].push(fields[1]); } else { - finalfilters[filters[i]] = [filters[i + 1]]; + finalfilters[fields[0]] = [fields[1]]; } } @@ -146,7 +152,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[0].split(" "); - oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word)); query = `(${query}) AND (${newWords.join(" ")})`; } else { @@ -154,7 +160,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[i].split(" "); - oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word)); const v = "(" + newWords.join(" ") + ")"; if (i === 0) { query = `(${query}) AND (${v}` + (values.length === 1 ? ")" : ""); @@ -183,7 +189,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc let newarray: Doc[] = []; while (docs.length > 0) { newarray = []; - docs.forEach(d => { + docs.filter(d => d).forEach(d => { const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); const data = d[annos ? fieldKey + "-annotations" : fieldKey]; @@ -351,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; @@ -495,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}`} @@ -503,7 +509,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc Logoff </div> </div> - <div className="searchBox-lozenge"> + <div className="searchBox-lozenge" onClick={() => DocServer.UPDATE_SERVER_CACHE()}> {`UI project`} </div> <div className="searchBox-lozenge-dashboard" > diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx index be53c0b9b..1b12e208c 100644 --- a/src/debug/Repl.tsx +++ b/src/debug/Repl.tsx @@ -7,7 +7,7 @@ import { makeInterface } from '../fields/Schema'; import { ObjectField } from '../fields/ObjectField'; import { RefField } from '../fields/RefField'; import { DocServer } from '../client/DocServer'; -import { resolvedPorts } from '../client/views/Main'; +import { resolvedPorts } from '../client/util/CurrentUserUtils'; @observer class Repl extends React.Component { diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index 0ca067ed3..bebd71dcf 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -14,7 +14,7 @@ import { RichTextField } from '../fields/RichTextField'; import { DateField } from '../fields/DateField'; import { ScriptField } from '../fields/ScriptField'; import CursorField from '../fields/CursorField'; -import { resolvedPorts } from '../client/views/Main'; +import { resolvedPorts } from '../client/util/CurrentUserUtils'; DateField; URLField; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 470f9d4be..4c3c45d92 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -19,42 +19,30 @@ import { DateField } from "./DateField"; import { listSpec } from "./Schema"; import { ComputedField, ScriptField } from "./ScriptField"; import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; -import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions } from "./util"; +import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions, normalizeEmail } from "./util"; import { LinkManager } from "../client/util/LinkManager"; 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 { const onDelegate = Object.keys(doc).includes(key); - const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - if (Field.IsField(field)) { - return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); - } - return ""; + return !Field.IsField(field) ? "" : (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); } export function toScriptString(field: Field): string { - if (typeof field === "string") { - return `"${field}"`; - } else if (typeof field === "number" || typeof field === "boolean") { - return String(field); - } else { - return field[ToScriptString](); - } + if (typeof field === "string") return `"${field}"`; + if (typeof field === "number" || typeof field === "boolean") return String(field); + return field[ToScriptString](); } export function toString(field: Field): string { - if (typeof field === "string") { - return field; - } else if (typeof field === "number" || typeof field === "boolean") { - return String(field); - } else if (field instanceof ObjectField) { - return field[ToString](); - } else if (field instanceof RefField) { - return field[ToString](); - } + if (typeof field === "string") return field; + if (typeof field === "number" || typeof field === "boolean") return String(field); + if (field instanceof ObjectField) return field[ToString](); + if (field instanceof RefField) return field[ToString](); return ""; } export function IsField(field: any): field is Field; @@ -86,16 +74,10 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); } -export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { - return Cast(field, Doc); -} +export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); } -export function DocListCast(field: FieldResult): Doc[] { - return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; -} -export function DocListCastOrNull(field: FieldResult) { - return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; -} +export function DocListCast(field: FieldResult) { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; } +export function DocListCastOrNull(field: FieldResult) { return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; } export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); @@ -103,15 +85,18 @@ export const DataSym = Symbol("Data"); export const LayoutSym = Symbol("Layout"); export const FieldsSym = Symbol("Fields"); export const AclSym = Symbol("Acl"); +export const AclUnset = Symbol("AclUnset"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); 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>([ + ["None", AclUnset], [SharingPermissions.None, AclPrivate], [SharingPermissions.View, AclReadonly], [SharingPermissions.Add, AclAddonly], @@ -119,25 +104,28 @@ const AclMap = new Map<string, symbol>([ [SharingPermissions.Admin, AclAdmin] ]); -export function fetchProto(doc: Doc) { +// caches the document access permissions for the current user. +// this recursively updates all protos as well. +export function updateCachedAcls(doc: Doc) { + if (!doc) return; const permissions: { [key: string]: symbol } = {}; - Object.keys(doc).filter(key => key.startsWith("acl")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!); - - if (Object.keys(permissions).length) doc[AclSym] = permissions; + doc[UpdatingFromServer] = true; + Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!)); + doc[UpdatingFromServer] = false; - if (GetEffectiveAcl(doc) === AclPrivate) { - runInAction(() => doc[FieldsSym](true)); + if (Object.keys(permissions).length) { + doc[AclSym] = permissions; } if (doc.proto instanceof Promise) { - doc.proto.then(fetchProto); + doc.proto.then(updateCachedAcls); return doc.proto; } } @scriptingGlobal -@Deserializable("Doc", fetchProto).withFields(["id"]) +@Deserializable("Doc", updateCachedAcls).withFields(["id"]) export class Doc extends RefField { constructor(id?: FieldId, forceSave?: boolean) { super(id); @@ -191,27 +179,26 @@ export class Doc extends RefField { } } private get __fieldKeys() { return this.___fieldKeys; } - private set __fieldKeys(value) { - this.___fieldKeys = value; - } - - @observable - private ___fields: any = {}; + private set __fieldKeys(value) { this.___fieldKeys = value; } - @observable - private ___fieldKeys: any = {}; - @observable - public [AclSym]: { [key: string]: symbol }; + @observable private ___fields: any = {}; + @observable private ___fieldKeys: any = {}; + @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; private [SelfProxy]: any; - public [FieldsSym](clear?: boolean) { return clear ? this.___fields = this.___fieldKeys = {} : this.___fields; } + public [FieldsSym](clear?: boolean) { + const self = this[SelfProxy]; + runInAction(() => clear && Array.from(Object.keys(self)).forEach(key => delete self[key])); + return this.___fields; + } public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); public [ToScriptString] = () => `DOC-"${this[Self][Id]}"-`; @@ -240,6 +227,7 @@ export class Doc extends RefField { private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; public static CurrentUserEmail: string = ""; + public static get CurrentUserEmailNormalized() { return normalizeEmail(Doc.CurrentUserEmail); } public async [HandleUpdate](diff: any) { const set = diff.$set; const sameAuthor = this.author === Doc.CurrentUserEmail; @@ -254,13 +242,16 @@ export class Doc extends RefField { const prev = GetEffectiveAcl(this); this[UpdatingFromServer] = true; this[fKey] = value; + this[UpdatingFromServer] = false; if (fKey.startsWith("acl")) { - fetchProto(this); + updateCachedAcls(this); } - this[UpdatingFromServer] = false; if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { DocServer.GetRefField(this[Id], true); } + // if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) { + // this[FieldsSym](true); + // } }; if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; @@ -332,7 +323,7 @@ export namespace Doc { export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult { try { return getField(doc[Self], key, ignoreProto); - } catch { + } catch { return doc; } } @@ -411,7 +402,7 @@ export namespace Doc { // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). export function GetProto(doc: Doc): Doc { if (doc instanceof Promise) { - console.log("GetProto: warning: got Promise insead of Doc"); + // console.log("GetProto: warning: got Promise insead of Doc"); } const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc)); return proto === doc ? proto : Doc.GetProto(proto); @@ -512,7 +503,6 @@ export namespace Doc { alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`); } alias.author = Doc.CurrentUserEmail; - alias[AclSym] = doc[AclSym]; Doc.AddDocToList(doc[DataSym], "aliases", alias); @@ -691,23 +681,22 @@ export namespace Doc { } else { 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]) { - 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. - newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances - newLayoutDoc.rootDocument = targetDoc; - const dataDoc = Doc.GetProto(targetDoc); - newLayoutDoc.resolvedDataDoc = dataDoc; - _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. + newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances + newLayoutDoc.rootDocument = targetDoc; + const dataDoc = Doc.GetProto(targetDoc); + newLayoutDoc.resolvedDataDoc = dataDoc; if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) { dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc }); } targetDoc[expandedLayoutFieldKey] = newLayoutDoc; _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args); - }); + })); } } } @@ -883,6 +872,7 @@ export namespace Doc { export class DocData { @observable _user_doc: Doc = undefined!; + @observable _sharing_doc: Doc = undefined!; @observable _searchQuery: string = ""; } @@ -895,21 +885,31 @@ 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); } export function UserDoc(): Doc { return manager._user_doc; } + export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, Doc, null); } + export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); } export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; } export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; } - export function SetUserDoc(doc: Doc) { manager._user_doc = doc; } + export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); } - export function IsSearchMatch(doc: Doc) { - return computedFn(function IsSearchMatch(doc: Doc) { - return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : - brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; - })(doc); - } + const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) { + return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : + brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; + }); + export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); } export function IsSearchMatchUnmemoized(doc: Doc) { return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; @@ -931,11 +931,9 @@ export namespace Doc { brushManager.SearchMatchDoc.clear(); } - export function IsBrushed(doc: Doc) { - return computedFn(function IsBrushed(doc: Doc) { - return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); - })(doc); - } + const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); }); + export function IsBrushed(doc: Doc) { return isBrushedCache(doc); } + // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) export function IsBrushedDegreeUnmemoized(doc: Doc) { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0; @@ -1024,6 +1022,8 @@ export namespace Doc { const fieldVal = doc[key]; if (Cast(fieldVal, listSpec("string"), []).length) { const vals = Cast(fieldVal, listSpec("string"), []); + const docs = vals.some(v => (v as any) instanceof Doc); + if (docs) return value === Field.toString(fieldVal as Field); return vals.some(v => v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } const fieldStr = Field.toString(fieldVal as Field); @@ -1064,10 +1064,11 @@ export namespace Doc { const container = target ?? CollectionDockingView.Instance.props.Document; const docFilters = Cast(container._docFilters, listSpec("string"), []); runInAction(() => { - for (let i = 0; i < docFilters.length; i += 3) { - if (docFilters[i] === key && (docFilters[i + 1] === value || modifiers === "match" || modifiers === "remove")) { - if (docFilters[i + 2] === modifiers && modifiers && docFilters[i + 1] === value) return; - docFilters.splice(i, 3); + for (let i = 0; i < docFilters.length; i++) { + const fields = docFilters[i].split(":"); // split key:value:modifier + if (fields[0] === key && (fields[1] === value || modifiers === "match" || modifiers === "remove")) { + if (fields[2] === modifiers && modifiers && fields[1] === value) return; + docFilters.splice(i, 1); container._docFilters = new List<string>(docFilters); break; } @@ -1076,9 +1077,7 @@ export namespace Doc { if (!docFilters.length && modifiers === "match" && value === undefined) { container._docFilters = undefined; } else if (modifiers !== "remove") { - docFilters.push(key); - docFilters.push(value); - docFilters.push(modifiers); + docFilters.push(key + ":" + value + ":" + modifiers); container._docFilters = new List<string>(docFilters); } } @@ -1099,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/List.ts b/src/fields/List.ts index c9e4bd3c1..215dff34b 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -43,7 +43,7 @@ const listHandlers: any = { } } const res = list.__fields.push(...items); - this[Update](); + this[Update]({ op: "$addToSet", items, length: length + items.length }); return res; }), reverse() { @@ -66,6 +66,7 @@ const listHandlers: any = { this[Self].__realFields(); // coerce retrieving entire array items = items.map(toObjectField); const list = this[Self]; + const removed = list.__fields.filter((item: any, i: number) => i >= start && i < start + deleteCount); for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -76,7 +77,8 @@ const listHandlers: any = { } } const res = list.__fields.splice(start, deleteCount, ...items); - this[Update](); + this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } : + items.length && !deleteCount && start === list.__fields.length ? { op: "$addToSet", items, length: list.__fields.length } : undefined); return res.map(toRealField); }), unshift(...items: any[]) { @@ -314,7 +316,7 @@ class ListImpl<T extends Field> extends ObjectField { // console.log(diff); const update = this[OnUpdate]; // update && update(diff); - update?.(); + update?.(diff); } private [Self] = this; 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 fe3eea69d..ecb3fb343 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,18 +1,19 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; -import { action, trace } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols"; +import { action, trace, } from "mobx"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; import { returnZero } from "../Utils"; import CursorField from "./CursorField"; -import { List } from "@material-ui/core"; - +import { List } from "./List"; +import { SnappingManager } from "../client/util/SnappingManager"; +import { computedFn } from "mobx-utils"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -23,6 +24,7 @@ export function TraceMobx() { tracing && trace(); } + export interface GetterResult { value: FieldResult; shouldReturn?: boolean; @@ -94,7 +96,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - UndoManager.AddEvent({ + !receiver[UpdatingFromServer] && UndoManager.AddEvent({ redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue }); @@ -112,9 +114,12 @@ export function makeReadOnly() { export function makeEditable() { _setter = _setterImpl; } -var _overrideAcl = false; -export function OVERRIDE_acl(val: boolean) { - _overrideAcl = val; + +export function normalizeEmail(email: string) { + return email.replace(/\./g, '__'); +} +export function denormalizeEmail(email: string) { + return email.replace(/__/g, '.'); } // playground mode allows the user to add/delete documents or make layout changes without them saving to the server @@ -124,14 +129,6 @@ export function OVERRIDE_acl(val: boolean) { // playgroundMode = !playgroundMode; // } -// the list of groups that the current user is a member of -let currentUserGroups: string[] = []; - -// called from GroupManager once the groups have been fetched from the server -export function setGroups(groups: string[]) { - currentUserGroups = groups; -} - /** * These are the various levels of access a user can have to a document. * @@ -139,7 +136,7 @@ export function setGroups(groups: string[]) { * * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. * - * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything. + * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything. * * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. * @@ -148,32 +145,38 @@ export function setGroups(groups: string[]) { export enum SharingPermissions { Admin = "Admin", Edit = "Can Edit", - Add = "Can Add", + Add = "Can Augment", View = "Can View", None = "Not Shared" } +// return acl from cache or cache the acl and return. +const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true); + /** * Calculates the effective access right to a document for the current user. */ -export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { - if (!target) return AclPrivate; - - // all changes received fromt the server must be processed as Admin - if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; +export function GetEffectiveAcl(target: any, user?: string): symbol { + return !target ? AclPrivate : + target[UpdatingFromServer] ? AclAdmin : getEffectiveAclCache(target, user);// all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) +} - // if the current user is the author of the document / the current user is a member of the admin group - const userChecked = user || Doc.CurrentUserEmail; - if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; - if (currentUserGroups.includes("Admin")) return AclAdmin; +function getPropAcl(target: any, prop: string | symbol | number) { + if (prop === UpdatingFromServer || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent + if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable + return GetEffectiveAcl(target); +} - if (target[AclSym] && Object.keys(target[AclSym]).length) { +let HierarchyMapping: Map<symbol, number> | undefined; - // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified) - if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; +function getEffectiveAcl(target: any, user?: string): symbol { + const targetAcls = target[AclSym]; + const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group + if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author + if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; - let effectiveAcl = AclPrivate; - const HierarchyMapping = new Map<symbol, number>([ + if (targetAcls && Object.keys(targetAcls).length) { + HierarchyMapping = HierarchyMapping || new Map<symbol, number>([ [AclPrivate, 0], [AclReadonly, 1], [AclAddonly, 2], @@ -181,17 +184,22 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, [AclAdmin, 4] ]); - for (const [key, value] of Object.entries(target[AclSym])) { + let effectiveAcl = AclPrivate; + for (const [key, value] of Object.entries(targetAcls)) { // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. - const entity = key.substring(4).replace('_', '.'); // an individual or a group - if (currentUserGroups.includes(entity) || userChecked === entity) { - if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { + const entity = denormalizeEmail(key.substring(4)); // an individual or a group + if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { + if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) { effectiveAcl = value as symbol; - if (effectiveAcl === AclAdmin) break; } } } + + // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document + const override = targetAcls["acl-Override"]; + if (override !== AclUnset && override !== undefined) effectiveAcl = override; + // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl) return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl; } @@ -213,7 +221,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc const HierarchyMapping = new Map<string, number>([ ["Not Shared", 0], ["Can View", 1], - ["Can Add", 2], + ["Can Augment", 2], ["Can Edit", 3], ["Admin", 4] ]); @@ -223,14 +231,17 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc const dataDoc = target[DataSym]; // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive - if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) { + if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) { target[key] = acl; layoutDocChanged = true; } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { - dataDoc[key] = acl; - dataDocChanged = true; + + if (GetEffectiveAcl(dataDoc) === AclAdmin) { + dataDoc[key] = acl; + dataDocChanged = true; + } // maps over the aliases of the document const links = DocListCast(dataDoc.links); @@ -238,41 +249,41 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { - if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d, inheritingFromCollection, visited); - } + // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d, inheritingFromCollection, visited); + // } const data = d[DataSym]; - if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, data, inheritingFromCollection, visited); } }); // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { - if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d, inheritingFromCollection, visited); - } + // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d, inheritingFromCollection, visited); + // } const data = d[DataSym]; - if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, data, inheritingFromCollection, visited); } }); } - layoutDocChanged && fetchProto(target); // updates target[AclSym] when changes to acls have been made - dataDocChanged && fetchProto(dataDoc); + layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made + dataDocChanged && updateCachedAcls(dataDoc); } const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - const effectiveAcl = GetEffectiveAcl(target, in_prop); + const effectiveAcl = getPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't - if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; - // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; + if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) return true; + // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { @@ -293,12 +304,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; - if (in_prop === "toString" || in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; - if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; - if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; - if (prop === LayoutSym) { - return target.__LAYOUT__; - } + if (in_prop === AclSym) return target[AclSym]; + if (in_prop === "toString" || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === "symbol")) return target.__fields[prop] || target[prop]; + if (GetEffectiveAcl(target) === AclPrivate) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; + if (prop === LayoutSym) return target.__LAYOUT__; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); @@ -354,20 +363,61 @@ export function deleteProperty(target: any, prop: string | number | symbol) { } export function updateFunction(target: any, prop: any, value: any, receiver: any) { - let current = ObjectField.MakeCopy(value); + let lastValue = ObjectField.MakeCopy(value); return (diff?: any) => { - if (true || !diff) { - diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; - const oldValue = current; - const newValue = ObjectField.MakeCopy(value); - current = newValue; - if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) { - UndoManager.AddEvent({ - redo() { receiver[prop] = newValue; }, - undo() { receiver[prop] = oldValue; } - }); - } + const op = + diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } : + diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } + : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; + !op.$set && ((op as any).length = diff.length); + const prevValue = ObjectField.MakeCopy(lastValue as List<any>); + lastValue = ObjectField.MakeCopy(value); + const newValue = ObjectField.MakeCopy(value); + + if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) { + !receiver[UpdatingFromServer] && UndoManager.AddEvent( + diff?.op === "$addToSet" ? + { + redo: () => { + receiver[prop].push(...diff.items.map((item: any) => item.value ? item.value() : item)); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + undo: action(() => { + 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]); + }) + } : + diff?.op === "$remFromSet" ? + { + redo: action(() => { + 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.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]); + } + } + : { + redo: () => { + receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); + lastValue = ObjectField.MakeCopy(receiver[prop]); + }, + undo: () => { + receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); + lastValue = ObjectField.MakeCopy(receiver[prop]); + } + }); } - target[Update](diff); + target[Update](op); }; }
\ No newline at end of file diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx index 81b3869ae..ebc8bc8a7 100644 --- a/src/mobile/AudioUpload.tsx +++ b/src/mobile/AudioUpload.tsx @@ -53,7 +53,7 @@ export class AudioUpload extends React.Component { * Pushing the audio doc onto Dash Web through the right side bar */ uploadAudio = () => { - const audioRightSidebar = Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; + const audioRightSidebar = Cast(Doc.SharingDoc(), Doc, null); const audioDoc = this._audioCol; const data = Cast(audioRightSidebar.data, listSpec(Doc)); for (let i = 1; i < 8; i++) { @@ -88,6 +88,7 @@ export class AudioUpload extends React.Component { rootSelected={returnTrue} removeDocument={undefined} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} onClick={undefined} ScreenToLocalTransform={Transform.Identity} diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 1ee4c7815..65f9e40ff 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -63,11 +63,11 @@ export class Uploader extends React.Component<ImageUploadProps> { doc = Docs.Create.ImageDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); } this.setOpacity(4, "1"); // Slab 4 - const res = await rp.get(Utils.prepend("/getUserDocumentId")); + const res = await rp.get(Utils.prepend("/getUserDocumentIds")); if (!res) { throw new Error("No user id returned"); } - const field = await DocServer.GetRefField(res); + const field = await DocServer.GetRefField(JSON.parse(res).userDocumentId); let pending: Opt<Doc>; if (field instanceof Doc) { pending = col; diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index cb5191e61..a42d85b56 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -220,6 +220,7 @@ export class MobileInterface extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} @@ -439,7 +440,7 @@ export class MobileInterface extends React.Component { // DocButton that uses UndoManager and handles the opacity change if CanUndo is true @computed get undo() { if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && - this._activeDoc !== Doc.UserDoc().mySharedDocs && this._activeDoc.title !== "WORKSPACES") { + this._activeDoc !== Doc.SharingDoc() && this._activeDoc.title !== "WORKSPACES") { return ( <div className="docButton" style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanUndo() ? "1" : "0.4", }} @@ -457,7 +458,7 @@ export class MobileInterface extends React.Component { // DocButton that uses UndoManager and handles the opacity change if CanRedo is true @computed get redo() { if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && - this._activeDoc !== Doc.UserDoc().mySharedDocs && this._activeDoc.title !== "WORKSPACES") { + this._activeDoc !== Doc.SharingDoc() && this._activeDoc.title !== "WORKSPACES") { return ( <div className="docButton" style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanRedo() ? "1" : "0.4", }} @@ -608,7 +609,7 @@ export class MobileInterface extends React.Component { // Returns the image upload pop up @computed get uploadImage() { - const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; + const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.SharingDoc(), Doc) as Doc; return <Uploader Document={doc} />; } @@ -631,7 +632,7 @@ export class MobileInterface extends React.Component { */ @action switchToMobileUploads = () => { - const mobileUpload = Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; + const mobileUpload = Cast(Doc.SharingDoc(), Doc) as Doc; this.switchCurrentView(mobileUpload); this._homeMenu = false; } diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx index 3d4680d58..4a1e26078 100644 --- a/src/mobile/MobileMain.tsx +++ b/src/mobile/MobileMain.tsx @@ -14,7 +14,7 @@ AssignAllExtensions(); await Docs.Prototypes.initialize(); if (info.id !== "__guest__") { // a guest will not have an id registered - await CurrentUserUtils.loadUserDocument(info); + await CurrentUserUtils.loadUserDocument(info.id); } document.getElementById('root')!.addEventListener('wheel', event => { if (event.ctrlKey) { 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/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index a52430ee8..775e90520 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -47,13 +47,20 @@ export class SearchManager extends ApiManager { const resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] }; let results: any; const dir = pathToDirectory(Directory.text); - results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, dir, ".txt$"); - for (const result in results) { - resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, "")); - resObj.lines.push(results[result].line); - resObj.numFound++; + try { + const regex = new RegExp(q.toString()); + results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, dir, ".txt$"); + for (const result in results) { + resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, "")); + resObj.lines.push(results[result].line); + resObj.numFound++; + } + res.send(resObj); + } catch (e) { + console.log(red("textsearch:bad RegExp" + q.toString())); + res.send([]); + return; } - res.send(resObj); } }); 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/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index 0d1d8f218..f36506b14 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -19,22 +19,53 @@ export default class UserManager extends ApiManager { method: Method.GET, subscription: "/getUsers", secureHandler: async ({ res }) => { - const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users"); + const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, "users"); const results = await cursor.toArray(); - res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId }))); + res.send(results.map(user => ({ email: user.email, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }))); } }); register({ + method: Method.POST, + subscription: "/setCacheDocumentIds", + secureHandler: async ({ user, req, res }) => { + const result: any = {}; + user.cacheDocumentIds = req.body.cacheDocumentIds; + user.save(err => { + if (err) { + result.error = [{ msg: "Error while caching documents" }]; + } + }); + + // Database.Instance.update(id, { "$set": { "fields.cacheDocumentIds": cacheDocumentIds } }, e => { + // console.log(e); + // }); + res.send(result); + } + }); + + register({ + method: Method.GET, + subscription: "/getUserDocumentIds", + secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }) + }); + + register({ + method: Method.GET, + subscription: "/getSharingDocumentId", + secureHandler: ({ res, user }) => res.send(user.sharingDocumentId) + }); + + register({ method: Method.GET, - subscription: "/getUserDocumentId", - secureHandler: ({ res, user }) => res.send(user.userDocumentId) + subscription: "/getLinkDatabaseId", + secureHandler: ({ res, user }) => res.send(user.linkDatabaseId) }); register({ method: Method.GET, subscription: "/getCurrentUser", - secureHandler: ({ res, user: { _id, email } }) => res.send(JSON.stringify({ id: _id, email })), + secureHandler: ({ res, user: { _id, email, cacheDocumentIds } }) => res.send(JSON.stringify({ id: _id, email, cacheDocumentIds })), publicHandler: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" })) }); 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/GarbageCollector.ts b/src/server/GarbageCollector.ts index a9a3b0481..7c441e3c0 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -65,7 +65,7 @@ async function GarbageCollect(full: boolean = true) { // await new Promise(res => setTimeout(res, 3000)); const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users'); const users = await cursor.toArray(); - const ids: string[] = users.map(user => user.userDocumentId); + const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId), ...users.map(user => user.linkDatabaseId)]; const visited = new Set<string>(); const files: { [name: string]: string[] } = {}; 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 00f1fe44e..3fbd4b3a7 100644 --- a/src/server/authentication/AuthenticationManager.ts +++ b/src/server/authentication/AuthenticationManager.ts @@ -47,7 +47,10 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { const model = { email, password, - userDocumentId: Utils.GenerateGuid() + userDocumentId: Utils.GenerateGuid(), + sharingDocumentId: Utils.GenerateGuid(), + linkDatabaseId: Utils.GenerateGuid(), + cacheDocumentIds: "" } as Partial<DashUserModel>; const user = new User(model); @@ -174,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' + @@ -247,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/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index 51d920a8f..bee28b96d 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -10,6 +10,9 @@ export type DashUserModel = mongoose.Document & { passwordResetExpires?: Date, userDocumentId: string; + sharingDocumentId: string; + linkDatabaseId: string; + cacheDocumentIds: string; profile: { name: string, @@ -35,7 +38,10 @@ const userSchema = new mongoose.Schema({ passwordResetToken: String, passwordResetExpires: Date, - userDocumentId: String, + userDocumentId: String, // id that identifies a document which hosts all of a user's account data + sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users + linkDatabaseId: String, + cacheDocumentIds: String, // set of document ids to retreive on startup facebook: String, twitter: String, 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 b33e76c0b..7d111f359 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -201,15 +201,18 @@ export namespace WebSocket { function setField(socket: Socket, newValue: Transferable) { Database.Instance.update(newValue.id, newValue, () => - socket.broadcast.emit(MessageStore.SetField.Message, newValue)); + socket.broadcast.emit(MessageStore.SetField.Message, newValue)); // broadcast set value to all other clients if (newValue.type === Types.Text) { // if the newValue has sring type, then it's suitable for searching -- pass it to SOLR Search.updateDocument({ id: newValue.id, data: { set: (newValue as any).data } }); } } + 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]) { @@ -271,33 +274,106 @@ export namespace WebSocket { return typeof value === "string" ? value : value[0]; } + function addToListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { + 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.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); + } + + function remFromListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { + diff.diff.$set = diff.diff.$remFromSet; delete diff.diff.$remFromSet; + const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; + const remListItems = diff.diff.$set[updatefield].fields; + const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; + diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem)); + 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("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 (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) { |