diff options
76 files changed, 1725 insertions, 1540 deletions
diff --git a/package-lock.json b/package-lock.json index 0eff1ff92..fa8f7259b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6483,6 +6483,15 @@ "resolved": "https://registry.npmjs.org/flexlayout-react/-/flexlayout-react-0.3.11.tgz", "integrity": "sha512-V+rEfyYJBqNk9oBgotPoXg8rmom4/ji9Mvr6f7T8sIJs83RuyK1D3oOrya2ydZIUCP2T6BIvj7rFTmRRkzpU8w==" }, + "fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=", + "requires": { + "async": ">=0.2.9", + "which": "^1.1.1" + } + }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", diff --git a/package.json b/package.json index 7a9ec6f99..7ea6fdc6f 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "find-in-files": "^0.5.0", "fit-curve": "^0.1.7", "flexlayout-react": "^0.3.11", + "fluent-ffmpeg": "^2.1.2", "formidable": "^1.2.1", "function-plot": "^1.22.7", "golden-layout": "^1.5.9", diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 851e78336..8565784b4 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -18,7 +18,7 @@ export enum DocumentType { LABEL = "label", // simple text label BUTTON = "button", // onClick button WEBCAM = "webcam", // webcam - PDFANNO = "pdfanno", // pdf text selection (could be just a collection?) + HTMLANCHOR = "htmlanchor", // text selection anchor in PDF/Web DATE = "date", // calendar view of a date SCRIPTING = "script", // script editor EQUATION = "equation", // equation editor diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 3e29a4edf..9bf3e7fc0 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,7 +1,7 @@ import { action, runInAction } from "mobx"; import { basename, extname } from "path"; import { DateField } from "../../fields/DateField"; -import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { HtmlField } from "../../fields/HtmlField"; import { InkField } from "../../fields/InkField"; @@ -58,8 +58,8 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { DocumentType } from "./DocumentTypes"; import { EquationBox } from "../views/nodes/EquationBox"; import { FunctionPlotBox } from "../views/nodes/FunctionPlotBox"; -import { script } from "googleapis/build/src/apis/script"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; +import { FieldViewProps } from "../views/nodes/FieldView"; const path = require('path'); const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); @@ -93,7 +93,7 @@ type PEVt = PEInfo | "none" | "all"; type DROPt = DAInfo | dropActionType; export class DocumentOptions { system?: BOOLt = new BoolInfo("is this a system created/owned doc"); - dropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto this doc "); + _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection "); targetDropAction?: DROPt = new DAInfo("what should happen to the source document when ??? "); color?: string; // foreground color data doc @@ -120,7 +120,7 @@ export class DocumentOptions { _showCaption?: string; // which field to display in the caption area. leave empty to have no caption _scrollTop?: number; // scroll location for pdfs _noAutoscroll?: boolean;// whether collections autoscroll when this item is dragged - _chromeStatus?: string; + _chromeHidden?: boolean; // whether the editing chrome for a document is hidden _layerTags?: List<string>; // layer tags a document has (used for tab filtering "layers" in document tab) _searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view) _forceActive?: boolean; // flag to handle pointer events when not selected (or otherwise active) @@ -156,12 +156,16 @@ export class DocumentOptions { y?: number; z?: number; // whether document is in overlay (1) or not (0 or undefined) author?: string; - layoutKey?: string; + _layoutKey?: string; type?: string; title?: string; + "acl-Public"?: string; // public permissions + "_acl-Public"?: string; // public permissions version?: string; // version identifier for a document label?: string; hidden?: boolean; + autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline. + dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it. toolTip?: string; // tooltip to display on hover dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel) description?: string; // added for links @@ -182,7 +186,7 @@ export class DocumentOptions { caption?: RichTextField; opacity?: number; defaultBackgroundColor?: string; - isLinkButton?: boolean; + _isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked isFolder?: boolean; lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection @@ -192,6 +196,8 @@ export class DocumentOptions { presProgressivize?: boolean; borderRounding?: string; boxShadow?: string; + data?: any; + baseProto?: boolean; // is this a base prototoype dontRegisterView?: boolean; lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. "onDoubleClick-rawScript"?: string; // onDoubleClick script in raw text form @@ -206,9 +212,16 @@ export class DocumentOptions { dockingConfig?: string; annotationOn?: Doc; isPushpin?: boolean; - removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document + _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document iconShape?: string; // shapes of the fonticon border + layout_linkView?: Doc; // view template for a link document + layout_keyValue?: string; // view tempalte for key value docs linkRelationship?: string; // type of relatinoship a link represents + linkDisplay?: boolean; // whether a link line should be dipslayed between the two link anchors + anchor1?: Doc; + anchor2?: Doc; + "anchor1-useLinkSmallAnchor"?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) + "anchor2-useLinkSmallAnchor"?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) ignoreClick?: boolean; onClick?: ScriptField; onDoubleClick?: ScriptField; @@ -304,47 +317,47 @@ export namespace Docs { const TemplateMap: TemplateMap = new Map([ [DocumentType.RTF, { layout: { view: FormattedTextBox, dataField: "text" }, - options: { _height: 150, _xMargin: 10, _yMargin: 10 } + options: { _height: 150, _xMargin: 10, _yMargin: 10, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.SEARCH, { layout: { view: SearchBox, dataField: defaultDataKey }, - options: { _width: 400 } + options: { _width: 400, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.FILTER, { layout: { view: FilterBox, dataField: defaultDataKey }, - options: { _width: 400 } + options: { _width: 400, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.COLOR, { layout: { view: ColorBox, dataField: defaultDataKey }, - options: { _nativeWidth: 220, _nativeHeight: 300 } + options: { _nativeWidth: 220, _nativeHeight: 300, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.IMG, { layout: { view: ImageBox, dataField: defaultDataKey }, - options: {} + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.WEB, { layout: { view: WebBox, dataField: defaultDataKey }, - options: { _height: 300, _fitWidth: true } + options: { _height: 300, _fitWidth: true, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, - options: { _panX: 0, _panY: 0, _viewScale: 1 } // , _width: 500, _height: 500 } + options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.KVP, { layout: { view: KeyValueBox, dataField: defaultDataKey }, - options: { _height: 150 } + options: { _fitWidth: true, _height: 150 } }], [DocumentType.VID, { layout: { view: VideoBox, dataField: defaultDataKey }, - options: { _currentTimecode: 0 }, + options: { _currentTimecode: 0, links: ComputedField.MakeFunction("links(self)") as any }, }], [DocumentType.AUDIO, { layout: { view: AudioBox, dataField: defaultDataKey }, - options: { _height: 35, backgroundColor: "lightGray" } + options: { _height: 35, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, - options: { _curPage: 1, _fitWidth: true } + options: { _curPage: 1, _fitWidth: true, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.IMPORT, { layout: { view: DirectoryImportBox, dataField: defaultDataKey }, @@ -352,7 +365,15 @@ export namespace Docs { }], [DocumentType.LINK, { layout: { view: LinkBox, dataField: defaultDataKey }, - options: { _height: 150, description: "" } + options: { + childDontRegisterViews: true, _isLinkButton: true, treeViewHideTitle: true, + treeViewOpen: true, _height: 150, description: "", + backgroundColor: "lightblue", // lightblue is default color for linking dot and link documents text comment area + treeViewExpandedView: "fields", _removeDropProperties: new List(["_layerTags", "isLinkButton"]), + links: ComputedField.MakeFunction("links(self)") as any, + linkBoxExcludedKeys: new List(["treeViewExpandedView", "aliases", "treeViewHideTitle", "_removeDropProperties", + "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]) + } }], [DocumentType.LINKDB, { data: new List<Doc>(), @@ -365,53 +386,62 @@ export namespace Docs { options: { childDropAction: "alias", title: "Global Script Database" } }], [DocumentType.SCRIPTING, { - layout: { view: ScriptingBox, dataField: defaultDataKey } + layout: { view: ScriptingBox, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.YOUTUBE, { layout: { view: YoutubeBox, dataField: defaultDataKey } }], [DocumentType.LABEL, { layout: { view: LabelBox, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.EQUATION, { layout: { view: EquationBox, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.FUNCPLOT, { layout: { view: FunctionPlotBox, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.BUTTON, { layout: { view: LabelBox, dataField: "onClick" }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.SLIDER, { layout: { view: SliderBox, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.PRES, { layout: { view: PresBox, dataField: defaultDataKey }, - options: {} + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.FONTICON, { layout: { view: FontIconBox, dataField: defaultDataKey }, - options: { _width: 40, _height: 40, borderRounding: "100%" }, + options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: ComputedField.MakeFunction("links(self)") as any }, }], - // [DocumentType.RECOMMENDATION, { - // layout: { view: RecommendationsBox, dataField: defaultDataKey }, - // options: { _width: 200, _height: 200 }, - // }], [DocumentType.WEBCAM, { - layout: { view: DashWebRTCVideo, dataField: defaultDataKey } + layout: { view: DashWebRTCVideo, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.PRESELEMENT, { layout: { view: PresElementBox, dataField: defaultDataKey } }], + [DocumentType.HTMLANCHOR, { + layout: { view: CollectionView, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } + }], [DocumentType.INK, { layout: { view: InkingStroke, dataField: defaultDataKey }, - options: { _fontFamily: "cursive", backgroundColor: "transparent" } + options: { _fontFamily: "cursive", backgroundColor: "transparent", links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.SCREENSHOT, { layout: { view: ScreenshotBox, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.COMPARISON, { layout: { view: ComparisonBox, dataField: defaultDataKey }, + options: { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.GROUPDB, { data: new List<Doc>(), @@ -419,15 +449,15 @@ export namespace Docs { options: { childDropAction: "alias", title: "Global Group Database" } }], [DocumentType.GROUP, { - layout: { view: EmptyBox, dataField: defaultDataKey } + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.TEXTANCHOR, { - layout: { view: EmptyBox, dataField: defaultDataKey } + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { targetDropAction: "move", links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } }] ]); - // All document prototypes are initialized with at least these values - const defaultOptions: DocumentOptions = { x: 0, y: 0, _width: 300 }; // bcz: do we really want to set anything here? could also try to set in render() methods for types that need a default const suffix = "Proto"; /** @@ -468,30 +498,22 @@ export namespace Docs { * @param type */ const PrototypeMap: PrototypeMap = new Map(); - export function get(type: DocumentType): Doc { - return PrototypeMap.get(type)!; - } + export function get(type: DocumentType): Doc { return PrototypeMap.get(type)!; } /** * A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here. */ - export function MainLinkDocument() { - return Prototypes.get(DocumentType.LINKDB); - } + export function MainLinkDocument() { return Prototypes.get(DocumentType.LINKDB); } /** * A collection of all scripts in the database */ - export function MainScriptDocument() { - return Prototypes.get(DocumentType.SCRIPTDB); - } + export function MainScriptDocument() { return Prototypes.get(DocumentType.SCRIPTDB); } /** - * A collection of all groups in the database + * A collection of all user acl groups in the database */ - export function MainGroupDocument() { - return Prototypes.get(DocumentType.GROUPDB); - } + export function MainGroupDocument() { return Prototypes.get(DocumentType.GROUPDB); } /** * This is a convenience method that is used to initialize @@ -517,14 +539,12 @@ export namespace Docs { const title = prototypeId.toUpperCase().replace(upper, `_${upper}`); // synthesize the default options, the type and title from computed values and // whatever options pertain to this specific prototype - 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 } as any); - doc.data = template.data; - doc.layout_keyValue = KeyValueBox.LayoutString(""); - return doc; + const options: DocumentOptions = { + system: true, _layoutKey: "layout", title, type, baseProto: true, x: 0, y: 0, _width: 300, ...(template.options || {}), + layout: layout.view?.LayoutString(layout.dataField), data: template.data, layout_keyValue: KeyValueBox.LayoutString("") + }; + return Doc.assign(new Doc(prototypeId, true), options as any, undefined, true); } - } /** @@ -611,8 +631,6 @@ export namespace Docs { Scripting.addGlobal(Buxton); - const delegateKeys = ["x", "y", "system", "layoutKey", "dropAction", "lockedPosiiton", "childDropAction", "isLinkButton", "removeDropProperties", "treeViewOpen"]; - /** * This function receives the relevant document prototype and uses * it to create a new of that base-level prototype, or the @@ -631,78 +649,47 @@ 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", protoId?: string) { - const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys, "^_"); - - protoProps.system = delegateProps.system; - - if (!("author" in protoProps)) protoProps.author = Doc.CurrentUserEmail; - - if (!("creationDate" in protoProps)) { - protoProps.creationDate = new DateField; - protoProps[`${fieldKey}-lastModified`] = new DateField; - } - - protoProps.isPrototype = true; - - const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey, protoId); - const viewDoc = Doc.MakeDelegate(dataDoc, delegId); - + function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data", protoId?: string) { + const viewKeys = ["x", "y", "system"]; // keys that should be addded to the view document even though they don't begin with an "_" + const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, "^_"); + + dataProps.system = viewProps.system; + dataProps.isPrototype = true; + dataProps.author = Doc.CurrentUserEmail; + dataProps.creationDate = new DateField; + dataProps[`${fieldKey}-lastModified`] = new DateField; + dataProps["acl-Override"] = "None"; + dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + dataProps[fieldKey] = data; // so that the list of annotations is already initialised, prevents issues in addonly. // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. + dataProps[fieldKey + "-annotations"] = new List<Doc>(); + const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); - dataDoc[fieldKey + "-annotations"] = new List<Doc>(); - - proto.links = ComputedField.MakeFunction("links(self)"); + viewProps.author = Doc.CurrentUserEmail; + viewProps["acl-Override"] = "None"; + viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true); + ![DocumentType.LINK, DocumentType.TEXTANCHOR, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(viewDoc); - viewDoc.author = Doc.CurrentUserEmail; - viewDoc.type !== DocumentType.LINK && viewDoc.type !== DocumentType.LABEL && DocUtils.MakeLinkToActiveAudio(viewDoc); + !Doc.IsSystem(dataDoc) && ![DocumentType.HTMLANCHOR, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR, DocumentType.TEXTANCHOR].includes(proto.type as any) && + !dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc); - viewDoc["acl-Public"] = dataDoc["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; - viewDoc["acl-Override"] = dataDoc["acl-Override"] = "None"; - - !Doc.IsSystem(dataDoc) && ![DocumentType.PDFANNO, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR, DocumentType.TEXTANCHOR].includes(proto.type as any) && - !dataDoc.isFolder && !protoProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc); - - return Doc.assign(viewDoc, delegateProps, true); - } - - /** - * This function receives the relevant top level document prototype - * and models a new instance by delegating from it. - * - * Note that it stores the data it recieves at the delegate's data key, - * and applies any document options to this new delegate / instance. - * @param proto the prototype from which to model this new delegate - * @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", id: string | undefined = undefined) { - const deleg = Doc.MakeDelegate(proto, id); - if (value !== undefined) { - deleg[fieldKey] = value; - } - return Doc.assign(deleg, options as any); + return viewDoc; } export function ImageDocument(url: string, options: DocumentOptions = {}) { const imgField = new ImageField(new URL(url)); - const inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options }); - let target = imgField.url.href; - if (new RegExp(window.location.origin).test(target)) { - const extension = path.extname(target); - target = `${target.substring(0, target.length - extension.length)}_o${extension}`; - } - return inst; + return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options }); } + export function PresDocument(initial: List<Doc> = new List(), options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.PRES), initial, options); } export function ScriptingDocument(script: Opt<ScriptField>, options: DocumentOptions = {}, fieldKey?: string) { - const res = InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script, options); - fieldKey && res.proto instanceof Doc && (res.proto.layout = ScriptingBox.LayoutString(fieldKey)); - return res; + return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script, + { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined }); } export function VideoDocument(url: string, options: DocumentOptions = {}) { @@ -722,13 +709,12 @@ export namespace Docs { } export function ComparisonDocument(options: DocumentOptions = { title: "Comparison Box" }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", options); } export function AudioDocument(url: string, options: DocumentOptions = {}) { - const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), { useLinkSmallAnchor: true, ...options }); // hideLinkButton: false, useLinkSmallAnchor: false, - Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'"); - return instance; + return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), + { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }); } export function SearchDocument(options: DocumentOptions = {}) { @@ -761,28 +747,18 @@ export namespace Docs { } export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) { - const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { - childDontRegisterViews: true, - isLinkButton: true, treeViewHideTitle: true, backgroundColor: "lightblue", // lightblue is default color for linking dot and link documents text comment area - treeViewExpandedView: "fields", removeDropProperties: new List(["_layerTags", "isLinkButton"]), ...options + const linkDoc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { + anchor1: source.doc, anchor2: target.doc, ...options }, id); - const linkDocProto = Doc.GetProto(doc); - linkDocProto.treeViewOpen = true;// setting this in the instance creator would set it on the view document. - linkDocProto.anchor1 = source.doc; - linkDocProto.anchor2 = target.doc; - - if (linkDocProto.linkBoxExcludedKeys === undefined) { - Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "aliases", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]); - Cast(linkDocProto.proto, Doc, null).layoutKey = undefined; - } - LinkManager.Instance.addLink(doc); + LinkManager.Instance.addLink(linkDoc); - return doc; + return linkDoc; } export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) { const I = new Doc(); + I[Initializing] = true; I.type = DocumentType.INK; I.layout = InkingStroke.LayoutString("data"); I.color = color; @@ -805,20 +781,16 @@ export namespace Docs { I.data = new InkField(points); I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; I["acl-Override"] = "None"; + I[Initializing] = false; return I; } export function PdfDocument(url: string, options: DocumentOptions = {}) { - const pdfProto = Prototypes.get(DocumentType.PDF); - pdfProto._fitWidth = true; // backward compatibility -- can be removed after db is reset - return InstanceFromProto(pdfProto, new PdfField(new URL(url)), { ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options); } export function WebDocument(url: string, options: DocumentOptions = {}) { - const webProto = Prototypes.get(DocumentType.WEB); - webProto.scrollHeight = 100000; // backward compatibility -- can be removed after db is reset - webProto._fitWidth = true; // backward compatibility -- can be removed after db is reset - return InstanceFromProto(webProto, url ? new WebField(new URL(url)) : undefined, { _chromeStatus: url ? undefined : "enabled", _lockedTransform: true, ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, options); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { @@ -826,61 +798,63 @@ export namespace Docs { } export function KVPDocument(document: Doc, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { _fitWidth: true, title: document.title + ".kvp", ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options }); } export function TextanchorDocument(options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.TEXTANCHOR), undefined, { targetDropAction: "move", ...options }, id); + return InstanceFromProto(Prototypes.get(DocumentType.TEXTANCHOR), undefined, options, id); } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Freeform }, id); + const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Freeform }, id); documents.map(d => d.context = inst); return inst; } + export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.HTMLANCHOR), new List(documents), options, id); + } export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id); } export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", ...options, _viewType: CollectionViewType.Linear }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id); } export function MapDocument(documents: Array<Doc>, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), options); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map }); } export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", _fitWidth: true, ...options, _viewType: CollectionViewType.Carousel }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel }); } export function Carousel3DDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", _fitWidth: true, ...options, _viewType: CollectionViewType.Carousel3D }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel3D }); } export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", _fitWidth: true, schemaHeaders: schemaHeaders.length ? new List(schemaHeaders) : undefined, ...options, _viewType: CollectionViewType.Schema }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema }); } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", _fitWidth: true, childDontRegisterViews: true, ...options, _viewType: CollectionViewType.Tree }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { childDontRegisterViews: true, ...options, _viewType: CollectionViewType.Tree }, id); } export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", _fitWidth: true, ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId); } export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Multicolumn }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn }); } export function MultirowDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Multirow }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multirow }); } - export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Masonry }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Masonry }); } export function LabelDocument(options?: DocumentOptions) { @@ -896,11 +870,6 @@ export namespace Docs { } export function ButtonDocument(options?: DocumentOptions) { - // const btn = InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); - // btn.layoutKey = "layout_onClick"; - // btn.height = 250; - // btn.width = 200; - // btn.layout_onClick = ScriptingBox.LayoutString("onClick"); return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); } @@ -908,9 +877,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SLIDER), undefined, { ...(options || {}) }); } - export function FontIconDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { hideLinkButton: true, ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); } export function FilterDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.FILTER), undefined, { ...(options || {}) }); @@ -921,11 +889,9 @@ export namespace Docs { } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { - const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: "remove|add", treeViewDefaultExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); const tabs = TreeDocument(documents, { title: "On-Screen Tabs", freezeChildren: "remove|add", treeViewLockExpandedView: true, treeViewDefaultExpandedView: "data", _fitWidth: true, system: true }); const all = TreeDocument([], { title: "Off-Screen Tabs", freezeChildren: "add", treeViewLockExpandedView: true, treeViewDefaultExpandedView: "data", system: true }); - Doc.GetProto(inst).data = new List<Doc>([tabs, all]); - return inst; + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", treeViewDefaultExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); } export function DirectoryImportDocument(options: DocumentOptions = {}) { @@ -1102,10 +1068,12 @@ export namespace DocUtils { options?.afterFocus?.(false); } - export let ActiveRecordings: AudioBox[] = []; + export let ActiveRecordings: { props: FieldViewProps, getAnchor: () => Doc }[] = []; - export function MakeLinkToActiveAudio(doc: Doc) { - DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d.getAnchor() || d.props.Document }, "audio link", "audio timeline")); + export function MakeLinkToActiveAudio(doc: Doc, broadcastEvent = true) { + broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1); + return DocUtils.ActiveRecordings.map(audio => + DocUtils.MakeLink({ doc: doc }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline")); } export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) { @@ -1113,42 +1081,46 @@ export namespace DocUtils { if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return; if (target.doc === Doc.UserDoc()) return undefined; + const makeLink = action((linkDoc: Doc, showPopup?: number[]) => { + if (showPopup) { + LinkManager.currentLink = linkDoc; - const makeLink = action((linkDoc: Doc, showPopup: number[]) => { - LinkManager.currentLink = linkDoc; + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = showPopup[0]; + TaskCompletionBox.popupY = showPopup[1] - 33; + TaskCompletionBox.taskCompleted = true; - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = showPopup[0]; - TaskCompletionBox.popupY = showPopup[1] - 33; - TaskCompletionBox.taskCompleted = true; + LinkDescriptionPopup.popupX = showPopup[0]; + LinkDescriptionPopup.popupY = showPopup[1]; + LinkDescriptionPopup.descriptionPopup = true; - LinkDescriptionPopup.popupX = showPopup[0]; - LinkDescriptionPopup.popupY = showPopup[1]; - LinkDescriptionPopup.descriptionPopup = true; + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } - const rect = document.body.getBoundingClientRect(); - if (LinkDescriptionPopup.popupX + 200 > rect.width) { - LinkDescriptionPopup.popupX -= 190; - TaskCompletionBox.popupX -= 40; + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); } - if (LinkDescriptionPopup.popupY + 100 > rect.height) { - LinkDescriptionPopup.popupY -= 40; - TaskCompletionBox.popupY -= 40; - } - - setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); + return linkDoc; }); - const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView", description }, id); - Doc.GetProto(linkDoc)["anchor1-useLinkSmallAnchor"] = source.doc.useLinkSmallAnchor; - Doc.GetProto(linkDoc)["anchor2-useLinkSmallAnchor"] = target.doc.useLinkSmallAnchor; - linkDoc.linkDisplay = true; - linkDoc.hidden = true; - Doc.GetProto(linkDoc)["acl-Public"] = linkDoc["acl-Public"] = SharingPermissions.Add; - linkDoc.layout_linkView = Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null); - Doc.GetProto(linkDoc).title = ComputedField.MakeFunction("generateLinkTitle(self)"); - showPopup && makeLink(linkDoc, showPopup); - return linkDoc; + return makeLink(Docs.Create.LinkDocument(source, target, { + title: ComputedField.MakeFunction("generateLinkTitle(self)") as any, + "anchor1-useLinkSmallAnchor": source.doc.useLinkSmallAnchor ? true : undefined, + "anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined, + "acl-Public": SharingPermissions.Add, + "_acl-Public": SharingPermissions.Add, + layout_linkView: Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null), + linkDisplay: true, hidden: true, + linkRelationship, + _layoutKey: "layout_linkView", + description + }, id), showPopup); } export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined { @@ -1377,7 +1349,7 @@ export namespace DocUtils { const pushpin = Docs.Create.FontIconDocument({ title: "pushpin", label: "", annotationOn: Cast(doc.annotationOn, Doc, null), isPushpin: true, icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), backgroundColor: "#ACCEF7", - _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) + _width: 15, _height: 15, _xPadding: 0, _isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) }); Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index 97ce13ae1..9cfea7f3f 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -1,4 +1,23 @@ (function ($) { + //if a tab is removed from the DOM and reinserted somewhere else all nested scrollTops seems to get reset to 0. (golden layout does this when a row structure needs to be converted to col or vice-versa) + const saveScrollTops = (element) => { + const children = Array.from(element.children()); + while (children.length) { + const child = children.pop(); + if (child.children) children.push(...(Array.from(child.children))); + if (child.scrollTop) child.preScrollTop = child.scrollTop; + } + } + const restoreScrollTops = (element) => { + const children = Array.from(element.children()); + while (children.length) { + const child = children.pop(); + if (child.children) children.push(...(Array.from(child.children))); + if (child.preScrollTop) { + child.scrollTop = child.preScrollTop; + } + } + } var lm = { "config": {}, "container": {}, "controls": {}, "errors": {}, "items": {}, "utils": {} }; lm.utils.F = function () { }; @@ -474,6 +493,8 @@ this._dragSources = []; this._updatingColumnsResponsive = false; this._firstLoad = true; + this.saveScrollTops = saveScrollTops; + this.restoreScrollTops = restoreScrollTops; this.width = null; this.height = null; @@ -2064,6 +2085,7 @@ this.element.find('.lm_tab').attr('title', lm.utils.stripTags(this._contentItem.config.title)); this.element.find('.lm_title').html(this._contentItem.config.title); this.childElementContainer = this.element.find('.lm_content'); + saveScrollTops(contentItem.element); this.childElementContainer.append(contentItem.element); this._updateTree(); @@ -2071,6 +2093,7 @@ this._setDimensions(); $(document.body).append(this.element); + restoreScrollTops(contentItem.element); var offset = this._layoutManager.container.offset(); @@ -2186,6 +2209,7 @@ this._contentItem._$destroy(); } + restoreScrollTops(this._contentItem.element) this.element.remove(); this._layoutManager.emit('itemDropped', this._contentItem); @@ -3211,7 +3235,10 @@ if (canDelete) { rowOrCol.removeChild(stack); if (rowOrCol.contentItems.length === 1 && parRowOrCol.contentItems.length === 1 && !parRowOrCol.isRoot) { + + saveScrollTops(rowOrCol.contentItems[0].element); parRowOrCol.replaceChild(rowOrCol, rowOrCol.contentItems[0]); + restoreScrollTops(rowOrCol.contentItems[0].element); } } } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 993eba889..ea27b7327 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -203,7 +203,7 @@ export class CurrentUserUtils { new SchemaHeaderField("[Long Description]", "dimGray", undefined, undefined, undefined, true), new SchemaHeaderField("[Details]", "dimGray", undefined, undefined, undefined, true), ]); - const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts, system: true }); + const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts, _chromeHidden: true, system: true }); detailView.isTemplateDoc = makeTemplate(detailView); details.title = "Details"; @@ -225,7 +225,7 @@ export class CurrentUserUtils { ]; if (doc["template-buttons"] === undefined) { doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, { - title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", + title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", _chromeHidden: true, hidden: ComputedField.MakeFunction("IsNoviceMode()") as any, _stayInCollection: true, _hideContextMenu: true, _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, @@ -367,7 +367,7 @@ export class CurrentUserUtils { }[] { if (doc.emptyPresentation === undefined) { doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), - { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _fitWidth: true, _width: 400, _height: 500, targetDropAction: "alias", _chromeStatus: "replaced", boxShadow: "0 0", system: true, cloneFieldFilter: new List<string>(["system"]) }); + { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _fitWidth: true, _width: 400, _height: 500, targetDropAction: "alias", _chromeHidden: true, boxShadow: "0 0", system: true, cloneFieldFilter: new List<string>(["system"]) }); ((doc.emptyPresentation as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyCollection === undefined) { @@ -423,7 +423,7 @@ export class CurrentUserUtils { ((doc.emptyScript as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyScreenshot === undefined) { - doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _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: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); @@ -453,7 +453,7 @@ export class CurrentUserUtils { { 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 }, { toolTip: "Tap to create a cat image in a new pane, drag for a cat image", title: "Image", icon: "cat", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyImage as Doc }, { toolTip: "Tap to create a comparison box in a new pane, drag for a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyComparison as Doc, noviceMode: true }, - { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc }, + { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc, noviceMode: true }, { toolTip: "Tap to create an audio recorder in a new pane, drag for an audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyAudio as Doc, noviceMode: true }, { toolTip: "Tap to create a button in a new pane, drag for a button", title: "Button", icon: "bolt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyButton as Doc }, { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Trails", icon: "pres-trail", click: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', drag: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, @@ -483,22 +483,22 @@ export class CurrentUserUtils { title, toolTip, ignoreClick, - dropAction: "alias", + _dropAction: "alias", onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined, onClick: click ? ScriptField.MakeScript(click) : undefined, backgroundColor, _hideContextMenu: true, - removeDropProperties: new List<string>(["_stayInCollection"]), + _removeDropProperties: new List<string>(["_stayInCollection"]), _stayInCollection: true, dragFactory, clickFactory, - hidden: noviceMode ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined, + hidden: !noviceMode ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined, system: true, })); if (dragCreatorSet === undefined) { doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { - title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, + title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true })); @@ -546,8 +546,8 @@ export class CurrentUserUtils { title, target, backgroundColor: "black", - dropAction: "alias", - removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + _dropAction: "alias", + _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), _width: 60, _height: 60, watchedDocuments, @@ -559,6 +559,7 @@ export class CurrentUserUtils { doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, { title: "menuItemPanel", childDropAction: "alias", + _chromeHidden: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), backgroundColor: "black", ignoreClick: true, _gridGap: 0, @@ -596,7 +597,7 @@ export class CurrentUserUtils { // Sets up mobileMenu stacking document static setupMobileMenu() { const menu = new PrefetchProxy(Docs.Create.StackingDocument(this.setupMobileButtons(), { - _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true + _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true, })); return menu; } @@ -626,27 +627,27 @@ export class CurrentUserUtils { // sets up the main document for the mobile button static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, { ...opts, - dropAction: undefined, removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, + _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, borderRounding: "5px", boxShadow: "0 0", system: true }) as any as Doc // sets up the text container for the information contained within the mobile button static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, { ...opts, - dropAction: undefined, removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, + _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true }) as any as Doc // Sets up the title of the button static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, { ...opts, - dropAction: undefined, title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", system: true + title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", system: true }) as any as Doc // Sets up the description of the button static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, { ...opts, - dropAction: undefined, title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true + title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true }) as any as Doc @@ -660,11 +661,12 @@ export class CurrentUserUtils { ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon, - dropAction: data.pointerDown ? "copy" : undefined, ignoreClick: data.ignoreClick, + _dropAction: data.pointerDown ? "copy" : undefined, ignoreClick: data.ignoreClick, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, clipboard: data.clipboard, onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, - backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory, system: true + backgroundColor: data.backgroundColor, + _removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory, system: true })); } @@ -689,13 +691,13 @@ export class CurrentUserUtils { static setupMobileUploadDoc(userDoc: Doc) { // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" }) const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", { - title: "Upload Images From the Web", _chromeStatus: "enabled", _lockedPosition: true, system: true + title: "Upload Images From the Web", _lockedPosition: true, system: true }); const uploadDoc = Docs.Create.StackingDocument([], { - title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, system: true + title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, system: true, _chromeHidden: true, }); return Docs.Create.StackingDocument([webDoc, uploadDoc], { - _width: screen.width, _lockedPosition: true, title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", system: true + _width: screen.width, _lockedPosition: true, title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", system: true, _chromeHidden: true, }); } @@ -715,20 +717,21 @@ export class CurrentUserUtils { if (doc.myCreators === undefined) { doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], { title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0, _fitWidth: true, - _width: 500, _height: 300, ignoreClick: true, _lockedPosition: true, system: true + _width: 500, _height: 300, ignoreClick: true, _lockedPosition: true, system: true, _chromeHidden: true, })); } // setup a color picker if (doc.myColorPicker === undefined) { const color = Docs.Create.ColorDocument({ - title: "color picker", _width: 300, dropAction: "alias", _hideContextMenu: true, _stayInCollection: true, _forceActive: true, removeDropProperties: new List<string>(["dropAction", "_stayInCollection", "_hideContextMenu", "forceActive"]), system: true + title: "color picker", _width: 220, _dropAction: "alias", _hideContextMenu: true, _stayInCollection: true, _forceActive: true, _removeDropProperties: new List<string>(["dropAction", "_stayInCollection", "_hideContextMenu", "forceActive"]), system: true }); doc.myColorPicker = new PrefetchProxy(color); } 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, ignoreClick: true, _lockedPosition: true, _forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true, + title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _forceActive: true, + system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, })) as any as Doc; doc.myTools = toolsStack; @@ -856,16 +859,16 @@ export class CurrentUserUtils { })) as any as Doc static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ - ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true + ...opts, _dropAction: "alias", _removeDropProperties: new List<string>(["_dropAction", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true })) as any as Doc /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupDockedButtons(doc: Doc) { if (doc["dockedBtn-undo"] === undefined) { - doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), dontUndo: true, _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true }); + doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), dontUndo: true, _stayInCollection: true, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true }); } if (doc["dockedBtn-redo"] === undefined) { - doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), dontUndo: true, _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true }); + doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), dontUndo: true, _stayInCollection: true, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true }); } if (doc.dockedBtns === undefined) { doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]); @@ -906,9 +909,8 @@ export class CurrentUserUtils { 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, + _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Add, "_acl-Public": SharingPermissions.Add, _chromeHidden: 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)"; @@ -922,13 +924,13 @@ export class CurrentUserUtils { if (doc.myImportDocs === undefined) { doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "My ImportDocuments", _forceActive: true, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, - childDropAction: "alias", _autoHeight: true, _yMargin: 50, _gridGap: 15, _lockedPosition: true, system: true + childDropAction: "alias", _autoHeight: true, _yMargin: 50, _gridGap: 15, _lockedPosition: true, system: true, _chromeHidden: true, })); } if (doc.myImportPanel === undefined) { const uploads = Cast(doc.myImportDocs, Doc, null); const newUpload = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("importDocument()"), toolTip: "Import External document", _stayInCollection: true, _hideContextMenu: true, title: "Import", icon: "upload", system: true }); - doc.myImportPanel = new PrefetchProxy(Docs.Create.StackingDocument([newUpload, uploads], { title: "My ImportPanel", _yMargin: 20, ignoreClick: true, _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, system: true })); + doc.myImportPanel = new PrefetchProxy(Docs.Create.StackingDocument([newUpload, uploads], { title: "My ImportPanel", _yMargin: 20, ignoreClick: true, _chromeHidden: true, _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, system: true })); } } diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index c6b654dda..a93b2f573 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -84,7 +84,13 @@ export namespace DictationManager { terminators: string[]; } + let pendingListen: Promise<string> | string | undefined; + export const listen = async (options?: Partial<ListeningOptions>) => { + if (pendingListen instanceof Promise) return pendingListen.then(pl => innerListen(options)); + return innerListen(options); + }; + const innerListen = async (options?: Partial<ListeningOptions>) => { let results: string | undefined; const overlay = options?.useOverlay; @@ -94,7 +100,8 @@ export namespace DictationManager { } try { - results = await listenImpl(options); + results = await (pendingListen = listenImpl(options)); + pendingListen = undefined; if (results) { Utils.CopyText(results); if (overlay) { @@ -106,6 +113,7 @@ export namespace DictationManager { options?.tryExecute && await DictationManager.Commands.execute(results); } } catch (e) { + console.log(e); if (overlay) { DictationOverlay.Instance.isListening = false; DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`; @@ -146,7 +154,8 @@ export namespace DictationManager { recognizer.onerror = (e: any) => { // e is SpeechRecognitionError but where is that defined? if (!(indefinite && e.error === "no-speech")) { recognizer.stop(); - reject(e); + resolve(e); + //reject(e); } }; @@ -158,8 +167,8 @@ export namespace DictationManager { recognizer.abort(); return complete(); } - handler && handler(current); - isManuallyStopped && complete(); + !isManuallyStopped && handler?.(current); + //isManuallyStopped && complete(); }; recognizer.onend = (e: Event) => { @@ -168,7 +177,7 @@ export namespace DictationManager { } if (current) { - sessionResults.push(current); + !isManuallyStopped && sessionResults.push(current); current = undefined; } recognizer.start(); @@ -199,14 +208,7 @@ export namespace DictationManager { } isListening = false; isManuallyStopped = true; - salvageSession ? recognizer.stop() : recognizer.abort(); - // let main = MainView.Instance; - // if (main.dictationOverlayVisible) { - // main.cancelDictationFade(); - // main.dictationOverlayVisible = false; - // main.dictationSuccess = undefined; - // setTimeout(() => main.dictatedPhrase = placeholder, 500); - // } + recognizer.stop(); // salvageSession ? recognizer.stop() : recognizer.abort(); }; const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 8cb80ecf9..8b37c9a6e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -14,6 +14,7 @@ export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @observable public DocumentViews: DocumentView[] = []; + @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView, b: DocumentView, l: Doc }[] = []; private static _instance: DocumentManager; @@ -28,8 +29,7 @@ export class DocumentManager { const whichOtherAnchor = view.props.LayoutTemplateString?.includes("anchor2") ? "anchor1" : "anchor2"; const otherDoc = link && (link[whichOtherAnchor] as Doc); const otherDocAnno = otherDoc?.type === DocumentType.TEXTANCHOR ? otherDoc.annotationOn as Doc : undefined; - otherDoc && DocumentManager.Instance.DocumentViews. - filter(dv => Doc.AreProtosEqual(dv.rootDoc, otherDoc) || Doc.AreProtosEqual(dv.rootDoc, otherDocAnno)). + otherDoc && DocumentManager.Instance.DocumentViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, otherDoc) || Doc.AreProtosEqual(dv.rootDoc, otherDocAnno)). forEach(otherView => { if (otherView.rootDoc.type !== DocumentType.LINK || otherView.props.LayoutTemplateString !== view.props.LayoutTemplateString) { this.LinkedDocumentViews.push({ a: whichOtherAnchor === "anchor1" ? otherView : view, b: whichOtherAnchor === "anchor1" ? view : otherView, l: link }); @@ -229,7 +229,10 @@ export class DocumentManager { } Scripting.addGlobal(function DocFocusOrOpen(doc: any) { const dv = DocumentManager.Instance.getDocumentView(doc); - if (dv && dv?.props.Document === doc) dv.props.focus(doc, { willZoom: true }); + if (dv && dv.props.Document === doc) { + dv.props.focus(doc, { willZoom: true }); + Doc.linkFollowHighlight(dv?.props.Document, false); + } else { const context = doc.context !== Doc.UserDoc().myFilesystem && Cast(doc.context, Doc, null); const showDoc = context || doc; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index dc95193ea..38d0ecaa6 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -303,7 +303,7 @@ export namespace DragManager { near = pt.pt; } }); - return { thisX: near[0], thisY: near[1] }; + return { x: near[0], y: near[1] }; } // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) { @@ -320,8 +320,8 @@ export namespace DragManager { return drag; }; return { - thisX: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.vertSnapLines()), - thisY: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()) + x: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.vertSnapLines()), + y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()) }; } export let docsBeingDragged: Doc[] = []; @@ -344,28 +344,22 @@ export namespace DragManager { dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); } - dragDiv.style.width = ""; - dragDiv.style.height = ""; - dragDiv.style.overflow = ""; + Object.assign(dragDiv.style, { width: "", height: "", overflow: "" }); dragDiv.hidden = false; - const scaleXs: number[] = []; - const scaleYs: number[] = []; - const xs: number[] = []; - const ys: number[] = []; + const scaleXs: number[] = [], scaleYs: number[] = [], xs: number[] = [], ys: number[] = []; docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const elesCont = { - left: Number.MAX_SAFE_INTEGER, - top: Number.MAX_SAFE_INTEGER, - right: Number.MIN_SAFE_INTEGER, - bottom: Number.MIN_SAFE_INTEGER + left: Number.MAX_SAFE_INTEGER, right: Number.MIN_SAFE_INTEGER, + top: Number.MAX_SAFE_INTEGER, bottom: Number.MIN_SAFE_INTEGER }; const dragElements = eles.map(ele => { if (!ele.parentNode) dragDiv.appendChild(ele); const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement; const rect = ele.getBoundingClientRect(); - const scaleX = rect.width / ele.offsetWidth, - scaleY = ele.offsetHeight ? rect.height / ele.offsetHeight : scaleX; + const scaleX = rect.width / ele.offsetWidth; + const scaleY = ele.offsetHeight ? rect.height / ele.offsetHeight : scaleX; + elesCont.left = Math.min(rect.left, elesCont.left); elesCont.top = Math.min(rect.top, elesCont.top); elesCont.right = Math.max(rect.right, elesCont.right); @@ -374,44 +368,34 @@ export namespace DragManager { ys.push(rect.top); scaleXs.push(scaleX); scaleYs.push(scaleY); - dragElement.style.opacity = "0.7"; - dragElement.style.position = "absolute"; - dragElement.style.margin = "0"; - dragElement.style.top = "0"; - dragElement.style.bottom = ""; - dragElement.style.left = "0"; - dragElement.style.color = "black"; - dragElement.style.transition = "none"; - dragElement.style.transformOrigin = "0 0"; - dragElement.style.borderRadius = getComputedStyle(ele).borderRadius; - dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000"; - dragElement.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`; - dragElement.style.width = `${rect.width / scaleX}px`; - dragElement.style.height = `${rect.height / scaleY}px`; + Object.assign(dragElement.style, { + opacity: "0.7", position: "absolute", margin: "0", top: "0", bottom: "", left: "0", color: "black", transition: "none", + borderRadius: getComputedStyle(ele).borderRadius, zIndex: globalCssVariables.contextMenuZindex, + transformOrigin: "0 0", width: `${rect.width / scaleX}px`, height: `${rect.height / scaleY}px`, + transform: `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`, + }); dragLabel.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0) - 20}px)`; if (docsBeingDragged.length) { const pdfBox = dragElement.getElementsByTagName("canvas"); const pdfBoxSrc = ele.getElementsByTagName("canvas"); Array.from(pdfBox).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); - const pdfView = dragElement.getElementsByClassName("pdfViewer-viewer"); - const pdfViewSrc = ele.getElementsByClassName("pdfViewer-viewer"); - const tops = Array.from(pdfViewSrc).map(p => p.scrollTop); - const oldopacity = dragElement.style.opacity; - dragElement.style.opacity = "0"; - setTimeout(() => { - dragElement.style.opacity = oldopacity; - Array.from(pdfView).map((v, i) => v.scrollTo({ top: tops[i] })); - }, 0); - } - if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none"; - const set = dragElement.getElementsByTagName('*'); - // tslint:disable-next-line: prefer-for-of - for (let i = 0; i < set.length; i++) { - set[i].hasAttribute("style") && ((set[i] as any).style.pointerEvents = "none"); } + [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => + ele.hasAttribute("style") && ((ele as any).style.pointerEvents = "none")); dragDiv.appendChild(dragElement); + if (dragElement !== ele) { + const children = [Array.from(ele.children), Array.from(dragElement.children)]; + while (children[0].length) { + const childs = [children[0].pop(), children[1].pop()]; + if (childs[0]?.children) { + children[0].push(...Array.from(childs[0].children)); + children[1].push(...Array.from(childs[1]!.children)); + } + if (childs[0]?.scrollTop) childs[1]!.scrollTop = childs[0].scrollTop; + } + } return dragElement; }); @@ -423,13 +407,26 @@ export namespace DragManager { options?.hideSource && hideDragShowOriginalElements(true); SnappingManager.SetIsDragging(true); - let lastX = downX; - let lastY = downY; + let lastPt = { x: downX, y: downY }; const xFromLeft = downX - elesCont.left; const yFromTop = downY - elesCont.top; const xFromRight = elesCont.right - downX; const yFromBottom = elesCont.bottom - downY; let scrollAwaiter: Opt<NodeJS.Timeout>; + + AbortDrag = () => { + options?.dragComplete?.(new DragCompleteEvent(true, dragData)); + endDrag(); + }; + + const endDrag = action(() => { + hideDragShowOriginalElements(false); + document.removeEventListener("pointermove", moveHandler, true); + document.removeEventListener("pointerup", upHandler); + SnappingManager.SetIsDragging(false); + SnappingManager.clearSnapLines(); + batch.end(); + }); const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { @@ -503,93 +500,39 @@ export namespace DragManager { scrollAwaiter = setTimeout(autoScrollHandler, 250); } - const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); + const { x, y } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); + const moveVec = { x: x - lastPt.x, y: y - lastPt.y }; + lastPt = { x, y }; - const moveX = thisX - lastX; - const moveY = thisY - lastY; - lastX = thisX; - lastY = thisY; - dragLabel.style.transform = `translate(${xs[0] + moveX + (options?.offsetX || 0)}px, ${ys[0] + moveY + (options?.offsetY || 0) - 20}px)`; + dragLabel.style.transform = `translate(${xs[0] + moveVec.x + (options?.offsetX || 0)}px, ${ys[0] + moveVec.y + (options?.offsetY || 0) - 20}px)`; dragElements.map((dragElement, i) => (dragElement.style.transform = - `translate(${(xs[i] += moveX) + (options?.offsetX || 0)}px, ${(ys[i] += moveY) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) + `translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) ); }; - - const endDrag = action(() => { - hideDragShowOriginalElements(false); - document.removeEventListener("pointermove", moveHandler, true); - document.removeEventListener("pointerup", upHandler); - SnappingManager.SetIsDragging(false); - SnappingManager.clearSnapLines(); - batch.end(); - }); - - AbortDrag = () => { - options?.dragComplete?.(new DragCompleteEvent(true, dragData)); - endDrag(); - }; const upHandler = (e: PointerEvent) => { - const complete = new DragCompleteEvent(false, dragData); - dispatchDrag(eles, e, complete, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag); - options?.dragComplete?.(complete); + dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options); endDrag(); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, complete: DragCompleteEvent, - xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) { - const removed = dragEles.map(dragEle => { - const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height, o: dragEle.style.overflow }; - dragEle.style.width = "0"; - dragEle.style.height = "0"; - dragEle.style.overflow = "hidden"; - return ret; - }); - dragDiv.hidden = true; - dragDiv.style.width = "0"; - dragDiv.style.height = "0"; - dragDiv.style.overflow = "hidden"; - const target = document.elementFromPoint(e.x, e.y); - removed.map(r => { - r.ele.style.width = r.w; - r.ele.style.height = r.h; - r.ele.style.overflow = r.o; - }); - const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); - if (target) { - target.dispatchEvent( - new CustomEvent<DropEvent>("dashPreDrop", { - bubbles: true, - detail: { - x: thisX, - y: thisY, - complete: complete, - shiftKey: e.shiftKey, - altKey: e.altKey, - metaKey: e.metaKey, - ctrlKey: e.ctrlKey, - embedKey: CanEmbed - } - }) - ); - finishDrag?.(complete); - target.dispatchEvent( - new CustomEvent<DropEvent>("dashOnDrop", { - bubbles: true, - detail: { - x: thisX, - y: thisY, - complete: complete, - shiftKey: e.shiftKey, - altKey: e.altKey, - metaKey: e.metaKey, - ctrlKey: e.ctrlKey, - embedKey: CanEmbed - } - }) - ); - } + function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number, y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions) { + const dropArgs = { + bubbles: true, + detail: { + ...pos, + complete, + shiftKey: e.shiftKey, + altKey: e.altKey, + metaKey: e.metaKey, + ctrlKey: e.ctrlKey, + embedKey: CanEmbed + } + }; + target.dispatchEvent(new CustomEvent<DropEvent>("dashPreDrop", dropArgs)); + finishDrag?.(complete); + target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", dropArgs)); + options?.dragComplete?.(complete); } -} +}
\ No newline at end of file diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index d9f010557..925b74efa 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -145,6 +145,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { title: `Import of ${directory}`, _width: 1105, _height: 500, + _chromeHidden: true, x: NumCast(doc.x), y: NumCast(doc.y) + offset }; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 0512864df..3c3d5c3b8 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,22 +1,21 @@ -import { runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { BoolCast, Cast, StrCast } from "../../fields/Types"; +import { Doc, DocListCast, Opt, DirectLinksSym, Field } from "../../fields/Doc"; +import { BoolCast, Cast, StrCast, PromiseValue } from "../../fields/Types"; import { LightboxView } from "../views/LightboxView"; import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView"; import { DocumentManager } from "./DocumentManager"; import { SharingManager } from "./SharingManager"; import { UndoManager } from "./UndoManager"; +import { observe, observable, reaction } from "mobx"; +import { listSpec } from "../../fields/Schema"; +import { List } from "../../fields/List"; +import { ProxyField } from "../../fields/Proxy"; type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; /* * link doc: - * - anchor1: doc - * - anchor1page: number - * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in + * - anchor1: doc * - anchor2: doc - * - anchor2page: number - * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in * * group doc: * - type: string representing the group type/name/category @@ -27,38 +26,84 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo */ export class LinkManager { - private static _instance: LinkManager; + @observable static _instance: LinkManager; + @observable static userDocs: Doc[] = []; public static currentLink: Opt<Doc>; - public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); } + public static get Instance() { return LinkManager._instance; } + constructor() { + LinkManager._instance = this; + setTimeout(() => { + LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)]; + const addLinkToDoc = (link: Doc): any => { + const a1 = link?.anchor1; + const a2 = link?.anchor2; + if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link))); + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].add(link); + Doc.GetProto(a2)[DirectLinksSym].add(link); + Doc.GetProto(link)[DirectLinksSym].add(link); + } + }; + const remLinkFromDoc = (link: Doc): any => { + const a1 = link?.anchor1; + const a2 = link?.anchor2; + if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link))); + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].delete(link); + Doc.GetProto(a2)[DirectLinksSym].delete(link); + Doc.GetProto(link)[DirectLinksSym].delete(link); + } + }; + const watchUserLinks = (userLinks: List<Doc>) => { + const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields + observe(userLinks, change => { + switch (change.type as any) { + case "splice": + (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); + (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); + break; + case "update": //let oldValue = change.oldValue; + } + }, true); + }; + observe(LinkManager.userDocs, change => { + switch (change.type as any) { + case "splice": (change as any).added.forEach(watchUserLinks); break; + case "update": //let oldValue = change.oldValue; + } + }, true); + }); + } - public addLink(linkDoc: Doc) { return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); } + public addLink(linkDoc: Doc) { + return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); + } public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor - public getAllDirectLinks(anchor: Doc): Doc[] { return this.directLinker(anchor); } // finds all links that contain the given anchor - public getAllLinks(): Doc[] { return this.allLinks(); } + public getAllDirectLinks(anchor: Doc): Doc[] { + return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); + } // finds all links that contain the given anchor + public getAllLinks(): Doc[] { return []; }//this.allLinks(); } - allLinks = computedFn(function allLinks(this: any): Doc[] { - const linkData = Doc.LinkDBDoc().data; - const lset = new Set<Doc>(DocListCast(linkData)); - 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)); - }); - }, true); + // allLinks = computedFn(function allLinks(this: any): Doc[] { + // const linkData = Doc.LinkDBDoc().data; + // const lset = new Set<Doc>(DocListCast(linkData)); + // SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); + // 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 Array.from(lset); + // }, true); relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { const lfield = Doc.LayoutFieldKey(anchor); return DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], - LinkManager.Instance.directLinker(anchor).slice()); + Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice());// LinkManager.Instance.directLinker(anchor).slice()); }, true); // returns map of group type to anchor's links in that group type @@ -77,13 +122,6 @@ export class LinkManager { return anchorGroups; } - // checks if a link with the given anchors exists - public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean { - 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 //TODO This should probably return undefined if there isn't an opposite anchor //TODO This should also await the return value of the anchor so we don't filter out promises @@ -99,14 +137,14 @@ export class LinkManager { // 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 + // if the target isn't onscreen, then it will open up the target in the lightbox, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); - public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean) => { + public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => { 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 createTabForTarget = (didFocus: boolean) => new Promise<ViewAdjustment>(res => { - const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation) || followLoc; + const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc); docViewProps.addDocTab(doc, where); setTimeout(() => { const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); @@ -132,7 +170,7 @@ export class LinkManager { docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); } }; - LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); + LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); } public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { @@ -159,7 +197,7 @@ export class LinkManager { const containerDoc = Cast(target.annotationOn, Doc, null) || target; const targetContext = Cast(containerDoc?.context, Doc, null); const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "add:right"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished); + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished); } } else { finished?.(); diff --git a/src/client/views/AudioWaveform.scss b/src/client/views/AudioWaveform.scss new file mode 100644 index 000000000..e20434a25 --- /dev/null +++ b/src/client/views/AudioWaveform.scss @@ -0,0 +1,17 @@ +.audioWaveform { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + z-index: -1000; + bottom: 0; + pointer-events: none; + div { + height: 100% !important; + width: 100% !important; + } + canvas { + height: 100% !important; + width: 100% !important; + } +} diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx new file mode 100644 index 000000000..7ff9c1071 --- /dev/null +++ b/src/client/views/AudioWaveform.tsx @@ -0,0 +1,61 @@ +import React = require("react"); +import axios from "axios"; +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import Waveform from "react-audio-waveform"; +import { Doc } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { listSpec } from "../../fields/Schema"; +import { Cast } from "../../fields/Types"; +import { numberRange } from "../../Utils"; +import "./AudioWaveform.scss"; + +export interface AudioWaveformProps { + duration: number; + mediaPath: string; + dataDoc: Doc; + PanelHeight: () => number; +} + +@observer +export class AudioWaveform extends React.Component<AudioWaveformProps> { + public static NUMBER_OF_BUCKETS = 100; + @computed get _waveHeight() { return Math.max(50, this.props.PanelHeight()); } + componentDidMount() { + const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []); + if (!audioBuckets.length) { + this.props.dataDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data + setTimeout(this.createWaveformBuckets); + } + } + // decodes the audio file into peaks for generating the waveform + createWaveformBuckets = async () => { + axios({ url: this.props.mediaPath, responseType: "arraybuffer" }) + .then(response => { + const context = new window.AudioContext(); + context.decodeAudioData(response.data, + action(buffer => { + const decodedAudioData = buffer.getChannelData(0); + const bucketDataSize = Math.floor(decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS); + const brange = Array.from(Array(bucketDataSize)); + this.props.dataDoc.audioBuckets = new List<number>( + numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map((i: number) => + brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2)); + })); + }); + } + + render() { + const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []); + return <div className="audioWaveform"> + <Waveform + color={"darkblue"} + height={this._waveHeight} + barWidth={0.1} + pos={this.props.duration} + duration={this.props.duration} + peaks={audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS ? audioBuckets : undefined} + progressColor={"blue"} /> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 876fbac54..be1eab86b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -84,7 +84,7 @@ export interface ViewBoxAnnotatableProps { } export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) { class Component extends Touchable<P> { - _annotationKey: string = "annotations"; + @observable _annotationKey: string = "annotations"; @observable _isChildActive = false; //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @@ -125,7 +125,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; } + @computed public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; } @action.bound removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean { @@ -203,8 +203,10 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); active = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool === InkTool.None && - (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false) - annotationsActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None || (this.props.layerProvider?.(this.props.Document) === false && this.props.active()) || + (this.props.rootSelected(outsideReaction) || + this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) + annotationsActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None || + (this.props.layerProvider?.(this.props.Document) === false && this.props.active()) || (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) } return Component; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 4a1ec4d6c..a5d80cd22 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -184,6 +184,18 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </div></Tooltip>; } @computed + get followLinkButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) : <Tooltip title={ + <div className="dash-tooltip">{"follow primary link on click"}</div>}> + <div className="documentButtonBar-linker" + style={{ color: targetDoc.isLinkButton ? "black" : "white", backgroundColor: targetDoc.isLinkButton ? "white" : "black" }} + onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false, false)))}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" /> + </div> + </Tooltip>; + } + @computed get pinButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={ @@ -202,7 +214,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV if (targetDoc) { TabDocView.PinDoc(targetDoc); 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 scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetDoc.type as any) || targetDoc._viewType === CollectionViewType.Stacking; const pannable: boolean = ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG); if (scrollable) { const scroll = targetDoc._scrollTop; @@ -231,9 +243,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV 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={() => this.pinWithView(targetDoc)}> + <div className="documentButtonBar-linker" onClick={() => this.pinWithView(targetDoc)}> {presPinWithViewIcon} </div> </Tooltip>; @@ -244,8 +254,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Open Sharing Manager"}</div></>}> <div className="documentButtonBar-linker" style={{ color: "white" }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="users" - /> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="users" /> </div></Tooltip >; } @@ -343,10 +352,10 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const considerPush = isText && this.considerGoogleDocsPush; return <div className="documentButtonBar"> <div className="documentButtonBar-button"> - <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> {DocumentLinksButton.StartLink || !Doc.UserDoc()["documentLinksButton-fullMenu"] ? <div className="documentButtonBar-button"> - <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> </div> : (null)} {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button"> {this.templateButton} @@ -357,6 +366,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="documentButtonBar-button"> {this.contextButton} </div> */} + {!SelectionManager.Views()?.some(v => v.allLinks.length) ? (null) : <div className="documentButtonBar-button"> + {this.followLinkButton} + </div>} <div className="documentButtonBar-button"> {this.pinButton} </div> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 6d8bd30be..a92891ee5 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -94,7 +94,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => { !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); this._edtingTitle = true; - setTimeout(() => this._keyinput.current!.focus(), 0); + this._keyinput.current && setTimeout(this._keyinput.current.focus); })); } @@ -103,13 +103,12 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const dragDocView = SelectionManager.Views()[0]; - const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document)); const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; - dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.ContentScale()).transformDirection(e.x - left, e.y - top); + const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction); + dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top); dragData.moveDocument = dragDocView.props.moveDocument; dragData.isSelectionMove = true; dragData.canEmbed = dragTitle; - dragData.dropAction = dragDocView.props.dropAction; this._hidden = this.Interacting = true; DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { dragComplete: action(e => { @@ -228,7 +227,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.Views()[0]; - let thisPt = { thisX: e.clientX - this._offX, thisY: e.clientY - this._offY }; + let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); @@ -250,10 +249,10 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); } - move[0] = thisPt.thisX - this._snapX; - move[1] = thisPt.thisY - this._snapY; - this._snapX = thisPt.thisX; - this._snapY = thisPt.thisY; + move[0] = thisPt.x - this._snapX; + move[1] = thisPt.y - this._snapY; + this._snapX = thisPt.x; + this._snapY = thisPt.y; let dragBottom = false, dragRight = false, dragBotRight = false; let dX = 0, dY = 0, dW = 0, dH = 0; switch (this._resizeHdlId) { @@ -433,7 +432,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b if (node.className === "mainView-mainContent") inMainMenuPanel = true; } const leftBounds = inMainMenuPanel ? 0 : this.props.boundsLeft; - const topBounds = this.props.boundsTop; + const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; const borderRadiusDraggerWidth = 15; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index bd67735a8..cbaa706e0 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -133,7 +133,7 @@ export class KeyManager { SelectionManager.DeselectAll(); LightboxView.SetLightboxDoc(undefined); } - DictationManager.Controls.stop(); + // DictationManager.Controls.stop(); GoogleAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close(); diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 731d46502..07ebe5fa4 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -114,9 +114,11 @@ export class LightboxView extends React.Component<LightboxViewProps> { @action public static Next() { const doc = LightboxView._doc!; const target = LightboxView._docTarget = LightboxView._future?.pop(); - const docView = target && DocumentManager.Instance.getLightboxDocumentView(target); - if (docView && target) { - docView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); + const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (targetDocView && target) { + const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target).lastElement(); + l && (Cast(l.anchor2, Doc, null).backgroundColor = "lightgreen"); + targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target }); } else { if (!target && LightboxView.path.length) { diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 92f6ae028..60327f1bf 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -7,6 +7,7 @@ import { DocServer } from "../DocServer"; import { AssignAllExtensions } from "../../extensions/General/Extensions"; import { Networking } from "../Network"; import { CollectionView } from "./collections/CollectionView"; +import { LinkManager } from "../util/LinkManager"; AssignAllExtensions(); @@ -31,5 +32,6 @@ AssignAllExtensions(); d.setTime(d.getTime() + (100 * 24 * 60 * 60 * 1000)); const expires = "expires=" + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; + new LinkManager(); ReactDOM.render(<MainView />, document.getElementById('root')); })();
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 9dba15f55..2f871c13d 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -102,7 +102,7 @@ export class MainView extends React.Component { } new InkStrokeProperties(); this._sidebarContent.proto = undefined; - DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's + DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeHidden"]); // 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))); @@ -195,8 +195,8 @@ export class MainView extends React.Component { document.addEventListener("pointerdown", this.globalPointerDown); document.addEventListener("click", (e: MouseEvent) => { if (!e.cancelBubble) { - const pathstr = (e as any)?.path.map((p: any) => p.classList?.toString()).join(); - if (pathstr.includes("libraryFlyout")) { + const pathstr = (e as any)?.path?.map((p: any) => p.classList?.toString()).join(); + if (pathstr?.includes("libraryFlyout")) { SelectionManager.DeselectAll(); } } @@ -234,7 +234,7 @@ export class MainView extends React.Component { })); } const pres = Docs.Create.PresDocument(new List<Doc>(), - { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias", _chromeStatus: "replaced", boxShadow: "0 0" }); + { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias", _chromeHidden: true, boxShadow: "0 0" }); CollectionDockingView.AddSplit(pres, "right"); this.userDoc.activePresentation = pres; Doc.AddDocToList(this.userDoc.myPresentations as Doc, "data", pres); diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 1e97f9b41..efdf7f9f5 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -1,6 +1,5 @@ -import { action, observable, runInAction, ObservableMap } from "mobx"; +import { action, observable, ObservableMap, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Dictionary } from "typescript-collections"; import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { List } from "../../fields/List"; @@ -8,7 +7,6 @@ import { NumCast } from "../../fields/Types"; import { GetEffectiveAcl } from "../../fields/util"; import { Utils } from "../../Utils"; import { Docs } from "../documents/Documents"; -import { DocumentType } from "../documents/DocumentTypes"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { DragManager } from "../util/DragManager"; import { undoBatch } from "../util/UndoManager"; @@ -22,8 +20,10 @@ const _global = (window /* browser */ || global /* node */) as any; export interface MarqueeAnnotatorProps { rootDoc: Doc; down: number[]; + iframe?: () => undefined | HTMLIFrameElement; scrollTop: number; scaling?: () => number; + iframeScaling?: () => number; containerOffset?: () => number[]; mainCont: HTMLDivElement; docView: DocumentView; @@ -60,8 +60,10 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width); this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop; this._height = this._width = 0; - document.addEventListener("pointermove", this.onSelectMove, true); - document.addEventListener("pointerup", this.onSelectEnd, true); + + const doc = (this.props.iframe?.()?.contentDocument ?? document); + doc.addEventListener("pointermove", this.onSelectMove); + doc.addEventListener("pointerup", this.onSelectEnd); AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.(this.highlight("rgba(173, 216, 230, 0.75)", true)); AnchorMenu.Instance.Highlight = this.highlight; @@ -92,19 +94,20 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { }); } componentWillUnmount() { - document.removeEventListener("pointermove", this.onSelectMove, true); - document.removeEventListener("pointerup", this.onSelectEnd, true); + const doc = (this.props.iframe?.()?.contentDocument ?? document); + doc.removeEventListener("pointermove", this.onSelectMove); + doc.removeEventListener("pointerup", this.onSelectEnd); } @undoBatch @action - makeAnnotationDocument = (color: string): Opt<Doc> => { + makeAnnotationDocument = (color: string, isLinkButton?: boolean): Opt<Doc> => { if (this.props.savedAnnotations.size === 0) return undefined; if ((Array.from(this.props.savedAnnotations.values())[0][0] as any).marqueeing) { const scale = this.props.scaling?.() || 1; const anno = Array.from(this.props.savedAnnotations.values())[0][0]; const containerOffset = this.props.containerOffset?.() || [0, 0]; - const marqueeAnno = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title }); + const marqueeAnno = Docs.Create.FreeformDocument([], { _isLinkButton: isLinkButton, backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title }); marqueeAnno.x = (parseInt(anno.style.left || "0") - containerOffset[0]) / scale; marqueeAnno.y = (parseInt(anno.style.top || "0") - containerOffset[1]) / scale + NumCast(this.props.scrollTop); marqueeAnno._height = parseInt(anno.style.height || "0") / scale; @@ -114,7 +117,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { return marqueeAnno; } - const textRegionAnno = Docs.Create.FreeformDocument([], { type: DocumentType.PDFANNO, annotationOn: this.props.rootDoc, title: "Selection on " + this.props.rootDoc.title, _width: 1, _height: 1 }); + const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, title: "Selection on " + this.props.rootDoc.title, _width: 1, _height: 1 }); let maxX = -Number.MAX_VALUE; let minY = Number.MAX_VALUE; const annoDocs: Doc[] = []; @@ -144,9 +147,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { highlight = (color: string, isLinkButton: boolean) => { // creates annotation documents for current highlights const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]); - const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton); annotationDoc && this.props.addDocument(annotationDoc); - annotationDoc && (annotationDoc.isLinkButton = isLinkButton); return annotationDoc as Doc ?? undefined; } @@ -170,9 +172,12 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { @action onSelectMove = (e: PointerEvent) => { // transform positions and find the width and height to set the marquee to - const boundingRect = this.props.mainCont.getBoundingClientRect(); - this._width = ((e.clientX - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width)) - this._startX; - this._height = ((e.clientY - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height)) - this._startY + this.props.mainCont.scrollTop; + const boundingRect = (this.props.iframe?.()?.contentDocument?.body || this.props.mainCont).getBoundingClientRect(); + const mainRect = this.props.mainCont.getBoundingClientRect(); + const cliX = e.clientX * (this.props.iframeScaling?.() || 1) - boundingRect.left; + const cliY = e.clientY * (this.props.iframeScaling?.() || 1) - boundingRect.top; + this._width = (cliX * (this.props.mainCont.offsetWidth / mainRect.width)) - this._startX; + this._height = (cliY * (this.props.mainCont.offsetHeight / mainRect.height)) - this._startY + this.props.mainCont.scrollTop; this._left = Math.min(this._startX, this._startX + this._width); this._top = Math.min(this._startY, this._startY + this._height); this._width = Math.abs(this._width); @@ -181,6 +186,9 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { } onSelectEnd = (e: PointerEvent) => { + const mainRect = this.props.mainCont.getBoundingClientRect(); + const cliX = e.clientX * (this.props.iframeScaling?.() || 1) + (this.props.iframe ? mainRect.left : 0); + const cliY = e.clientY * (this.props.iframeScaling?.() || 1) + (this.props.iframe ? mainRect.top : 0); if (this._width > 10 || this._height > 10) { // configure and show the annotation/link menu if a the drag region is big enough const marquees = this.props.mainCont.getElementsByClassName("marqueeAnnotator-dragBox"); if (marquees?.length) { // copy the temporary marquee to allow for multiple selections (not currently available though). @@ -200,7 +208,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { MarqueeAnnotator.previewNewAnnotation(this.props.savedAnnotations, this.props.annotationLayer, copy, this.props.getPageFromScroll?.(this._top) || 0); } - AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); + AnchorMenu.Instance.jumpTo(cliX, cliY); if (AnchorMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up this.highlight("rgba(245, 230, 95, 0.75)", false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color) @@ -208,7 +216,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { this.props.finishMarquee(); } else { runInAction(() => this._width = this._height = 0); - this.props.finishMarquee(e.clientX, e.clientY); + this.props.finishMarquee(cliX, cliY); } } diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 7a83295c7..5c8b5f985 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -32,7 +32,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { propertyToggleBtn = (label: string, property: string, tooltip: (on?: any) => string, icon: (on: boolean) => string, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void) => { const targetDoc = this.selectedDoc; - const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => (dv?.layoutDoc || doc)[prop] = (dv?.layoutDoc || doc)[prop] ? undefined : true; + const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => (dv?.layoutDoc || doc)[prop] = (dv?.layoutDoc || doc)[prop] ? false : true; return !targetDoc ? (null) : <Tooltip title={<div className={`dash-tooltip`}>{tooltip(targetDoc?.[property])} </div>} placement="top"> <div> @@ -74,7 +74,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn("Caption", "_showCaption", on => `${on ? "Hide" : "Show"} caption footer`, on => "closed-captioning", (dv, doc) => (dv?.rootDoc || doc)._showCaption = (dv?.rootDoc || doc)._showCaption === undefined ? "caption" : undefined); } @computed get chromeButton() { - return this.propertyToggleBtn("Controls", "_chromeStatus", on => `${on === "enabled" ? "Hide" : "Show"} editing UI`, on => "edit", (dv, doc) => (dv?.rootDoc || doc)._chromeStatus = (dv?.rootDoc || doc)._chromeStatus === undefined ? "enabled" : undefined); + return this.propertyToggleBtn("Controls", "_chromeHidden", on => `${on ? "Show" : "Hide"} editing UI`, on => "edit", (dv, doc) => (dv?.rootDoc || doc)._chromeHidden = !(dv?.rootDoc || doc)._chromeHidden); } @computed get titleButton() { return this.propertyToggleBtn("Title", "_showTitle", on => "Switch between title styles", on => "text-width", (dv, doc) => (dv?.rootDoc || doc)._showTitle = !(dv?.rootDoc || doc)._showTitle ? "title" : (dv?.rootDoc || doc)._showTitle === "title" ? "title:hover" : undefined); @@ -165,10 +165,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { } @computed get onPerspectiveFlyout() { - const excludedViewTypes = Doc.UserDoc().noviceMode ? [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Stacking, CollectionViewType.Map, CollectionViewType.Linear] : - [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Linear]; + const excludedViewTypes = [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Linear]; - const makeLabel = (value: string, label: string) => <div className="radio"> + const makeLabel = (value: string, label: string) => <div className="radio" key={label}> <label> <input type="radio" value={value} checked={(this.selectedDoc?._viewType ?? "invalid") === value} onChange={this.handlePerspectiveChange} /> {label} diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 1e96d62d2..470e51835 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -311,7 +311,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { addDocument={returnFalse} moveDocument={undefined} removeDocument={returnFalse} - parentActive={() => false} + parentActive={returnFalse} whenActiveChanged={emptyFunction} addDocTab={returnFalse} pinToPres={emptyFunction} diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss new file mode 100644 index 000000000..9686cce85 --- /dev/null +++ b/src/client/views/SidebarAnnos.scss @@ -0,0 +1,24 @@ +.sidebarAnnos-tagList { + display: flex; + flex-direction: row; + overflow: auto; + flex-flow: row; + flex-wrap: wrap; + .sidebarAnnos-filterTag, .sidebarAnnos-filterTag-active, + .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active { + font-weight: bold; + padding-left: 6; + padding-right: 6; + box-shadow: black 1px 1px 4px; + border-radius: 5; + margin: 2; + height: 20; + background-color: lightgrey; + } + .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active { + border-radius: 15px; + } + .sidebarAnnos-filterTag-active { + background-color: white; + } +}
\ No newline at end of file diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx new file mode 100644 index 000000000..6b0b928b3 --- /dev/null +++ b/src/client/views/SidebarAnnos.tsx @@ -0,0 +1,140 @@ +import { computed } from 'mobx'; +import { observer } from "mobx-react"; +import { Doc, DocListCast, StrListCast, Opt } from "../../fields/Doc"; +import { Id } from '../../fields/FieldSymbols'; +import { List } from '../../fields/List'; +import { NumCast, StrCast } from '../../fields/Types'; +import { emptyFunction, OmitKeys, returnOne, returnTrue, returnZero } from '../../Utils'; +import { Docs, DocUtils } from '../documents/Documents'; +import { Transform } from '../util/Transform'; +import { CollectionStackingView } from './collections/CollectionStackingView'; +import { CollectionViewType } from './collections/CollectionView'; +import { FieldViewProps } from './nodes/FieldView'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { SearchBox } from './search/SearchBox'; +import "./SidebarAnnos.scss"; +import { StyleProp } from './StyleProvider'; +import React = require("react"); +import { DocumentViewProps } from './nodes/DocumentView'; + +interface ExtraProps { + fieldKey: string; + layoutDoc: Doc; + rootDoc: Doc; + dataDoc: Doc; + annotationsActive: (outsideReaction: boolean) => boolean; + whenActiveChanged: (isActive: boolean) => void; + ScreenToLocalTransform: () => Transform; + sidebarAddDocument: (doc: (Doc | Doc[]), suffix: string) => boolean; + removeDocument: (doc: (Doc | Doc[]), suffix: string) => boolean; + moveDocument: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string) => boolean; +} +@observer +export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { + _stackRef = React.createRef<CollectionStackingView>(); + @computed get allHashtags() { + const keys = new Set<string>(); + DocListCast(this.props.rootDoc[this.sidebarKey()]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key))); + return Array.from(keys.keys()).filter(key => key[0]).filter(key => !key.startsWith("_") && (key[0] === "#" || key[0] === key[0].toUpperCase())).sort(); + } + @computed get allUsers() { + const keys = new Set<string>(); + DocListCast(this.props.rootDoc[this.sidebarKey()]).forEach(doc => keys.add(StrCast(doc.author))); + return Array.from(keys.keys()).sort(); + } + get filtersKey() { return "_" + this.sidebarKey() + "-docFilters"; } + + anchorMenuClick = (anchor: Doc) => { + const startup = StrListCast(this.props.rootDoc.docFilters).map(filter => filter.split(":")[0]).join(" "); + const target = Docs.Create.TextDocument(startup, { + title: "anno", + annotationOn: this.props.rootDoc, _width: 200, _height: 50, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), + _fontFamily: StrCast(Doc.UserDoc().fontFamily) + }); + FormattedTextBox.SelectOnLoad = target[Id]; + FormattedTextBox.DontSelectInitialText = true; + this.allHashtags.map(tag => target[tag] = tag); + DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup", "annotation"); + this.addDocument(target); + this._stackRef.current?.focusDocument(target); + } + makeDocUnfiltered = (doc: Doc) => { + if (DocListCast(this.props.rootDoc[this.sidebarKey()]).includes(doc)) { + if (this.props.layoutDoc[this.filtersKey]) { + this.props.layoutDoc[this.filtersKey] = new List<string>(); + return true; + } + } + return false; + } + sidebarKey = () => this.props.fieldKey + "-sidebar"; + filtersHeight = () => 50; + screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.props.dataDoc), 0).scale(this.props.scaling?.() || 1); + panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 : (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth); + panelHeight = () => this.props.PanelHeight() - this.filtersHeight(); + addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey()); + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey()); + removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey()); + docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; + + sidebarStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => { + if (property === StyleProp.ShowTitle) return StrCast(this.props.layoutDoc["sidebar-childShowTitle"], "title"); + return this.props.styleProvider?.(doc, props, property); + } + render() { + const renderTag = (tag: string) => { + const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`); + return <div key={tag} className={`sidebarAnnos-filterTag${active ? "-active" : ""}`} + onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, "check", true, this.sidebarKey(), e.shiftKey)}> + {tag} + </div>; + }; + const renderUsers = (user: string) => { + const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`author:${user}:check`); + return <div key={user} className={`sidebarAnnos-filterUser${active ? "-active" : ""}`} + onClick={e => Doc.setDocFilter(this.props.rootDoc, "author", user, "check", true, this.sidebarKey(), e.shiftKey)}> + {user} + </div>; + }; + return !this.props.layoutDoc._showSidebar ? (null) : + <div style={{ + position: "absolute", pointerEvents: this.props.active() ? "all" : undefined, top: 0, right: 0, + background: this.props.styleProvider?.(this.props.rootDoc, this.props, StyleProp.WidgetColor), + width: `${this.panelWidth()}px`, + height: "100%" + }}> + <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight(), width: this.panelWidth() }}> + {this.allUsers.map(renderUsers)} + {this.allHashtags.map(renderTag)} + </div> + <div style={{ width: "100%", height: this.panelHeight(), position: "relative" }}> + <CollectionStackingView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} ref={this._stackRef} + NativeWidth={returnZero} + NativeHeight={returnZero} + PanelHeight={this.panelHeight} + PanelWidth={this.panelWidth} + xMargin={0} + yMargin={0} + styleProvider={this.sidebarStyleProvider} + docFilters={this.docFilters} + scaleField={this.sidebarKey() + "-scale"} + isAnnotationOverlay={false} + select={emptyFunction} + active={this.props.annotationsActive} + scaling={returnOne} + whenActiveChanged={this.props.whenActiveChanged} + childHideDecorationTitle={returnTrue} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.screenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + viewType={CollectionViewType.Stacking} + fieldKey={this.sidebarKey()} + pointerEvents={"all"} + /> + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 2585db046..0460addf3 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -72,13 +72,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | const isBackground = () => StrListCast(doc?._layerTags).includes(StyleLayers.Background); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); + const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); switch (property.split(":")[0]) { case StyleProp.TreeViewIcon: return doc ? Doc.toIcon(doc) : "question"; case StyleProp.DocContents: return undefined; case StyleProp.WidgetColor: return isAnnotated ? "lightBlue" : darkScheme() ? "lightgrey" : "dimgrey"; case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null)); - case StyleProp.HideLinkButton: return isAnchor || props?.dontRegisterView || ((!selected || doc?.type === DocumentType.PDFANNO) && (doc?.isLinkButton || doc?.hideLinkButton)); + case StyleProp.HideLinkButton: return props?.dontRegisterView || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); case StyleProp.ShowTitle: return doc && !doc.presentationTargetDoc && StrCast(doc._showTitle, !Doc.IsSystem(doc) && doc.type === DocumentType.RTF ? (doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : "author;creationDate") : "") || ""; @@ -92,7 +93,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | case StyleProp.Hidden: return BoolCast(doc?._hidden); case StyleProp.BorderRounding: return StrCast(doc?._borderRounding); case StyleProp.TitleHeight: return 15; - case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) || doc?.type === DocumentType.RTF) && doc?._showTitle && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; + case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) || + doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; case StyleProp.BackgroundColor: { if (isAnchor && docProps) return "transparent"; if (isCaption) return "rgba(0,0,0 ,0.4)"; @@ -120,6 +122,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | case DocumentType.LINK: return "transparent"; case DocumentType.WEB: case DocumentType.PDF: + case DocumentType.SCREENSHOT: case DocumentType.VID: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 23d8baeaf..dc6e2fe65 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -80,8 +80,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @undoBatch @action toggleChrome = (): void => { - this.props.docViews.map(dv => Doc.Layout(dv.layoutDoc)).forEach(layout => - layout._chromeStatus = (layout._chromeStatus ? undefined : StrCast(layout._replacedChrome, "enabled"))); + this.props.docViews.map(dv => Doc.Layout(dv.layoutDoc)).forEach(layout => layout._chromeHidden = !layout._chromeHidden); } // todo: add brushes to brushMap to save with a style name @@ -115,11 +114,11 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const addedTypes = Doc.UserDoc().noviceMode ? [] : DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); const layout = Doc.Layout(firstDoc); const templateMenu: Array<JSX.Element> = []; - this.props.templates.forEach((checked, template) => + this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />)); templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />); templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); - !Doc.UserDoc().noviceMode && templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); + !Doc.UserDoc().noviceMode && templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={!layout._chromeHidden} toggle={this.toggleChrome} />); addedTypes.concat(noteTypes).map(template => template.treeViewChecked = this.templateIsUsed(firstDoc, template)); this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push( <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />)); diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index c5a05da00..f0b9b5240 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -172,7 +172,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}> {this.content} </div> - {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} + {this.props.Document._chromeHidden ? (null) : this.buttons} <div className="dot-bar"> {this.dots} </div> diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 512328835..f400ac5a2 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -109,7 +109,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) render() { return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> {this.content} - {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} + {!this.props.Document._chromeHidden ? (null) : this.buttons} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 57f4555a1..7e89cf55d 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -188,6 +188,8 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { // if not going in a row layout, must add already existing content into column const rowlayout = instance._goldenLayout.root.contentItems[0]; const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); + + CollectionDockingView.Instance._goldenLayout.saveScrollTops(rowlayout.element); rowlayout.parent.replaceChild(rowlayout, newColumn); if (pullSide === "top") { newColumn.addChild(rowlayout, undefined, true); @@ -196,6 +198,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { newColumn.addChild(newContentItem, undefined, true); newColumn.addChild(rowlayout, 0, true); } + CollectionDockingView.Instance._goldenLayout.restoreScrollTops(rowlayout.element); rowlayout.config.height = 50; newContentItem.config.height = 50; @@ -210,8 +213,9 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { // if not going in a row layout, must add already existing content into column const collayout = instance._goldenLayout.root.contentItems[0]; const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); - collayout.parent.replaceChild(collayout, newRow); + CollectionDockingView.Instance._goldenLayout.saveScrollTops(collayout.element); + collayout.parent.replaceChild(collayout, newRow); if (pullSide === "left") { newRow.addChild(collayout, undefined, true); newRow.addChild(newContentItem, 0, true); @@ -219,6 +223,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { newRow.addChild(newContentItem, undefined, true); newRow.addChild(collayout, 0, true); } + CollectionDockingView.Instance._goldenLayout.restoreScrollTops(collayout.element); collayout.config.width = 50; newContentItem.config.width = 50; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 581520619..ba701b2a4 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -26,7 +26,7 @@ interface CMVFieldRowProps { rows: () => number; headings: () => object[]; Document: Doc; - chromeStatus: string; + chromeHidden?: boolean; heading: string; headingObject: SchemaHeaderField | undefined; docList: Doc[]; @@ -190,7 +190,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { if (e.button === 0 && !e.ctrlKey) { - setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => !this.props.chromeStatus && this.collapseSection(e)); + setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => !this.props.chromeHidden && this.collapseSection(e)); this._createAliasSelected = false; } } @@ -254,8 +254,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @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)))); - const chromeStatus = this.props.chromeStatus; - const showChrome = (chromeStatus !== 'view-mode' && chromeStatus); + const showChrome = !this.props.chromeHidden; const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; return this.collapsed ? (null) : <div style={{ position: "relative" }}> @@ -288,7 +287,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr } @computed get headingView() { - const noChrome = !this.props.chromeStatus; + const noChrome = this.props.chromeHidden; const key = this.props.pivotField; const evContents = this.heading ? this.heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const editableHeaderView = <EditableView diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 6a9ad27c0..aaf243567 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -517,7 +517,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </button> </Tooltip> {this.subChrome} - {/* {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} */} + {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} {!this._buttonizableCommands ? (null) : this.templateChrome} </div> </div> diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 22b5c2b2a..6f6cdd5d2 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -14,17 +14,17 @@ import React = require("react"); @observer export class CollectionPileView extends CollectionSubView(doc => doc) { - _originalChrome: string = ""; + _originalChrome: any = ""; componentDidMount() { if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") { this.Document._pileLayoutEngine = "pass"; } - this._originalChrome = StrCast(this.layoutDoc._chromeStatus); - this.layoutDoc._chromeStatus = undefined; + this._originalChrome = this.layoutDoc._chromeHidden; + this.layoutDoc._chromeHidden = true; } componentWillUnmount() { - this.layoutDoc._chromeStatus = this._originalChrome; + this.layoutDoc._chromeHidden = this._originalChrome; } layoutEngine = () => StrCast(this.Document._pileLayoutEngine); @@ -119,7 +119,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { render() { return <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} - style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> + style={{ width: this.props.PanelWidth(), height: "100%" }}> {this.contents} </div>; } diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index cc56831f3..19ad5cefa 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -58,4 +58,12 @@ left: 0; } } + + .collectionStackedTimeline-waveform { + position: absolute; + width: 100%; + top: 0; + left: 0; + pointer-events: none; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 16a1c02f7..c0cebf021 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,23 +1,26 @@ import React = require("react"); -import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, Opt, DocListCast } from "../../../fields/Doc"; +import { Doc, DocListCast } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent } from "../../../Utils"; +import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent, returnTrue } from "../../../Utils"; import { Docs } from "../../documents/Documents"; +import { LinkManager } from "../../util/LinkManager"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; +import { AudioWaveform } from "../AudioWaveform"; import { CollectionSubView } from "../collections/CollectionSubView"; -import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView"; +import { LightboxView } from "../LightboxView"; +import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; -import { Transform } from "../../util/Transform"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -32,6 +35,7 @@ export type CollectionStackedTimelineProps = { isChildActive: () => boolean; startTag: string; endTag: string; + mediaPath: string; }; @observer @@ -59,9 +63,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument super(props); // onClick play scripts CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; - CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; - CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; } componentDidMount() { document.addEventListener("keydown", this.keyEvents, true); } @@ -77,9 +79,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument } toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); rangeClickScript = () => CollectionStackedTimeline.RangeScript; - labelClickScript = () => CollectionStackedTimeline.LabelScript; rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; - labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript; // for creating key anchors with key events @action @@ -173,14 +173,14 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument @action playOnClick = (anchorDoc: Doc, clientX: number) => { - const seekTimeInSeconds = this.anchorStart(anchorDoc); + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; const endTime = this.anchorEnd(anchorDoc); - if (this.layoutDoc.autoPlay) { + if (this.layoutDoc.autoPlayAnchors) { if (this.props.playing()) this.props.Pause(); else this.props.playFrom(seekTimeInSeconds, endTime); } else { if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) { - if (!this.layoutDoc.autoPlay && this.props.playing()) { + if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) { this.props.Pause(); } else { this.props.Play(); @@ -194,45 +194,24 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument @action clickAnchor = (anchorDoc: Doc, clientX: number) => { - const seekTimeInSeconds = this.anchorStart(anchorDoc); + if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false); + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; const endTime = this.anchorEnd(anchorDoc); if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { if (this.props.playing()) this.props.Pause(); - else if (this.layoutDoc.autoPlay) this.props.Play(); - else if (!this.layoutDoc.autoPlay) { + else if (this.layoutDoc.autoPlayAnchors) this.props.Play(); + else if (!this.layoutDoc.autoPlayAnchors) { const rect = this._timeline?.getBoundingClientRect(); rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); } } else { - if (this.layoutDoc.autoPlay) this.props.playFrom(seekTimeInSeconds, endTime); + if (this.layoutDoc.autoPlayAnchors) this.props.playFrom(seekTimeInSeconds, endTime); else this.props.setTime(seekTimeInSeconds); } return { select: true }; } - // starting the drag event for anchor resizing - onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { - this._timeline?.setPointerCapture(e.pointerId); - const newTime = (e: PointerEvent) => { - const rect = (e.target as any).getBoundingClientRect(); - return this.toTimeline(e.clientX - rect.x, rect.width); - }; - const changeAnchor = (anchor: Doc, left: boolean, time: number) => { - const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined; - if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); - else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; - return false; - }; - setupMoveUpEvents(this, e, - (e) => changeAnchor(anchor, left, newTime(e)), - (e) => { - this.props.setTime(newTime(e)); - this._timeline?.releasePointerCapture(e.pointerId); - }, - emptyFunction); - } - // makes sure no anchors overlaps each other by setting the correct position and width getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => { const timelineContentWidth = this.props.PanelWidth(); @@ -255,7 +234,154 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument return level; } - renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + dictationHeight = () => this.props.PanelHeight() / 3; + timelineContentHeight = () => this.props.PanelHeight() * 2 / 3; + dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight()); + @computed get renderDictation() { + const dictation = Cast(this.dataDoc[this.props.fieldKey.replace("annotations", "dictation")], Doc, null); + return !dictation ? (null) : <div style={{ position: "absolute", height: this.dictationHeight(), top: this.timelineContentHeight(), background: "tan" }}> + <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + Document={dictation} + PanelHeight={this.dictationHeight} + isAnnotationOverlay={true} + select={emptyFunction} + scaling={returnOne} + xMargin={25} + yMargin={10} + ScreenToLocalTransform={this.dictationScreenToLocalTransform} + whenActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1}> + </DocumentView> + </div>; + } + @computed get renderAudioWaveform() { + return !this.props.mediaPath ? (null) : + <div className="collectionStackedTimeline-waveform" > + <AudioWaveform + duration={this.duration} + mediaPath={this.props.mediaPath} + dataDoc={this.dataDoc} + PanelHeight={this.timelineContentHeight} /> + </div>; + } + currentTimecode = () => this.currentTime; + render() { + const timelineContentWidth = this.props.PanelWidth(); + const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; + const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); + const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; + const isActive = this.props.isChildActive() || this.props.isSelected(false); + return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline} + onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}> + {drawAnchors.map(d => { + const start = this.anchorStart(d.anchor); + const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration); + const left = start / this.duration * timelineContentWidth; + const top = d.level / maxLevel * this.timelineContentHeight(); + const timespan = end - start; + return this.props.Document.hideAnchors ? (null) : + <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]} + style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${this.timelineContentHeight() / maxLevel}px` }} + onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} > + <StackedTimelineAnchor {...this.props} + mark={d.anchor} + rangeClickScript={this.rangeClickScript} + rangePlayScript={this.rangePlayScript} + left={left} + top={top} + width={timelineContentWidth * timespan / this.duration} + height={this.timelineContentHeight() / maxLevel} + toTimeline={this.toTimeline} + layoutDoc={this.layoutDoc} + currentTimecode={this.currentTimecode} + _timeline={this._timeline} + stackedTimeline={this} + /> + </div>; + })} + {this.selectionContainer} + {this.renderAudioWaveform} + {this.renderDictation} + + <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} /> + </div>; + } +} + +interface StackedTimelineAnchorProps { + mark: Doc; + rangeClickScript: () => ScriptField; + rangePlayScript: () => ScriptField; + left: number; + top: number; + width: number; + height: number; + toTimeline: (screen_delta: number, width: number) => number; + playLink: (linkDoc: Doc) => void; + setTime: (time: number) => void; + isChildActive: () => boolean; + startTag: string; + endTag: string; + renderDepth: number; + layoutDoc: Doc; + ScreenToLocalTransform: () => Transform; + _timeline: HTMLDivElement | null; + focus: DocFocusFunc; + currentTimecode: () => number; + isSelected: (outsideReaction?: boolean) => boolean; + stackedTimeline: CollectionStackedTimeline; +} +@observer +class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> { + _lastTimecode: number; + _disposer: IReactionDisposer | undefined; + constructor(props: any) { + super(props); + this._lastTimecode = this.props.currentTimecode(); + } + componentDidMount() { + this._disposer = reaction(() => this.props.currentTimecode(), + (time) => { + const dictationDoc = Cast(this.props.layoutDoc["data-dictation"], Doc, null); + const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc); + if ((isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc)) && DocListCast(this.props.mark.links).length && + time > NumCast(this.props.mark[this.props.startTag]) && + time < NumCast(this.props.mark[this.props.endTag]) && + this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) { + LinkManager.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true); + } + this._lastTimecode = time; + }); + } + componentWillUnmount() { + this._disposer?.(); + } + // starting the drag event for anchor resizing + onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { + this.props._timeline?.setPointerCapture(e.pointerId); + const newTime = (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + return this.props.toTimeline(e.clientX - rect.x, rect.width); + }; + const changeAnchor = (anchor: Doc, left: boolean, time: number) => { + const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined; + if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); + else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; + return false; + }; + setupMoveUpEvents(this, e, + (e) => changeAnchor(anchor, left, newTime(e)), + (e) => { + this.props.setTime(newTime(e)); + this.props._timeline?.releasePointerCapture(e.pointerId); + }, + emptyFunction); + } + renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { const anchor = observable({ view: undefined as any }); const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => { this.props.playLink(mark); @@ -276,54 +402,23 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument parentActive={out => this.props.isSelected(out) || this.props.isChildActive()} rootSelected={returnFalse} onClick={script} - onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript} + onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript} ignoreAutoHeight={false} hideResizeHandles={true} bringToFront={emptyFunction} - scriptContext={this} /> + scriptContext={this.props.stackedTimeline} /> }; }); - renderAnchor = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const inner = this.renderInner(mark, script, doublescript, x, y, width, height); + render() { + const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.props.left, this.props.top, this.props.width, this.props.height); return <> {inner.view} {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : <> - <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, mark, true)} /> - <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, mark, false)} /> + <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} /> + <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} /> </>} </>; - }); - - render() { - const timelineContentWidth = this.props.PanelWidth(); - const timelineContentHeight = this.props.PanelHeight(); - const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; - const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); - const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - const isActive = this.props.isChildActive() || this.props.isSelected(false); - return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline} - onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}> - {drawAnchors.map(d => { - const start = this.anchorStart(d.anchor); - const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration); - const left = start / this.duration * timelineContentWidth; - const top = d.level / maxLevel * timelineContentHeight; - const timespan = end - start; - return this.props.Document.hideAnchors ? (null) : - <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]} - style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${timelineContentHeight / maxLevel}px` }} - onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} > - {this.renderAnchor(d.anchor, this.rangeClickScript, this.rangePlayScript, - left, - top, - timelineContentWidth * timespan / this.duration, - timelineContentHeight / maxLevel)} - </div>; - })} - {this.selectionContainer} - <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} /> - </div>; } } Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 3f821bbcc..23c63561c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -36,7 +36,7 @@ type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentS const StackingDocument = makeInterface(collectionSchema, documentSchema); export type collectionStackingViewProps = { - chromeStatus?: string; + chromeHidden?: boolean; viewType?: CollectionViewType; NativeWidth?: () => number; NativeHeight?: () => number; @@ -53,7 +53,7 @@ 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 chromeHidden() { return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null); } @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); } @@ -63,7 +63,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking; } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.pivotField && (this.chromeStatus !== 'view-mode' && this.chromeStatus)); } + @computed get showAddAGroup() { return this.pivotField && !this.chromeHidden; } @computed get columnWidth() { return Math.min(this.props.PanelWidth() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); @@ -223,11 +223,12 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, styleProvider={this.styleProvider} layerProvider={this.props.layerProvider} docViewPath={this.props.docViewPath} + fitWidth={this.props.childFitWidth} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} freezeDimensions={this.props.childFreezeDimensions} - NativeWidth={this.props.childIgnoreNativeSize ? returnZero : doc._fitWidth && !Doc.NativeWidth(doc) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox - NativeHeight={this.props.childIgnoreNativeSize ? returnZero : doc._fitWidth && !Doc.NativeHeight(doc) ? height : undefined} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.() || doc._fitWidth && !Doc.NativeWidth(doc) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.() || doc._fitWidth && !Doc.NativeHeight(doc) ? height : undefined} dontCenter={this.props.childIgnoreNativeSize ? "xy" : undefined} dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} @@ -260,13 +261,13 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, const y = this._scroll; // required for document decorations to update when the text box container is scrolled const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined); // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off - return new Transform(- translateX - (dref?.centeringX || 0), - translateY - (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale); + return new Transform(- translateX + (dref?.centeringX || 0), - translateY + (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale); } getDocWidth(d?: Doc) { if (!d) return 0; const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const maxWidth = this.columnWidth / this.numGroupColumns; - if (!this.layoutDoc._columnsFill && !childLayoutDoc._fitWidth) { + if (!this.layoutDoc._columnsFill && !(childLayoutDoc._fitWidth || this.props.childFitWidth?.())) { return Math.min(d[WidthSym](), maxWidth); } return maxWidth; @@ -276,8 +277,8 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const childDataDoc = (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc; const maxHeight = (lim => lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim)(NumCast(this.layoutDoc.childLimitHeight, -1)); - const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!childLayoutDoc._fitWidth ? d[WidthSym]() : 0); - const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!childLayoutDoc._fitWidth ? d[HeightSym]() : 0); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.()) ? d[WidthSym]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.()) ? d[HeightSym]() : 0); if (nw && nh) { const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid); @@ -286,7 +287,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, docWid * nh / nw); } const childHeight = NumCast(childLayoutDoc._height); - const panelHeight = this.props.PanelHeight() - 2 * this.yMargin; + const panelHeight = (childLayoutDoc._fitWidth || this.props.childFitWidth?.()) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; return Math.min(childHeight, maxHeight, panelHeight); } @@ -412,7 +413,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, } }} addDocument={this.addDocument} - chromeStatus={this.chromeStatus} + chromeHidden={this.chromeHidden} columnHeaders={this.columnHeaders} Document={this.props.Document} DataDoc={this.props.DataDoc} @@ -445,7 +446,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, return <CollectionMasonryViewFieldRow showHandle={first} Document={this.props.Document} - chromeStatus={this.chromeStatus} + chromeHidden={this.chromeHidden} pivotField={this.pivotField} unobserveHeight={(ref) => this.refList.splice(this.refList.indexOf(ref), 1)} observeHeight={(ref) => { @@ -534,10 +535,6 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, ref={this.createRef} style={{ overflowY: this.props.active() ? "auto" : "hidden", - transform: `scale(${this.scaling}`, - height: `${1 / this.scaling * 100}%`, - width: `${1 / this.scaling * 100}%`, - transformOrigin: "top left", background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), pointerEvents: this.backgroundEvents ? "all" : undefined }} @@ -551,11 +548,11 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {/* {!this.chromeStatus || !this.props.isSelected() ? (null) : + {/* {this.chromeHidden || !this.props.isSelected() ? (null) : <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.chromeStatus !== 'view-mode'} + defaultChecked={true} checkedChildren="edit" unCheckedChildren="view" />} */} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 70ec1f925..bee7aeb87 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -32,7 +32,7 @@ interface CSVFieldColumnProps { docList: Doc[]; heading: string; pivotField: string; - chromeStatus: string; + chromeHidden?: boolean; columnHeaders: SchemaHeaderField[] | undefined; headingObject: SchemaHeaderField | undefined; yMargin: number; @@ -166,7 +166,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <div className="colorOptions"> {colors.map(col => { const palette = PastelSchemaPalette.get(col); - return <div className={"colorPicker" + (selected === palette ? " active" : "")} style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} /> + return <div className={"colorPicker" + (selected === palette ? " active" : "")} + style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />; })} </div> </div>; @@ -249,8 +250,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC style={{ marginTop: this.props.yMargin, width: (this.props.columnWidth) / - ((uniqueHeadings.length + - ((this.props.chromeStatus !== 'view-mode' && this.props.chromeStatus) ? 1 : 0)) || 1) + ((uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1)) || 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. @@ -307,7 +307,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC }}> {this.props.renderChildren(this.props.docList)} </div> - {(this.props.chromeStatus !== 'view-mode' && this.props.chromeStatus && type !== DocumentType.PRES) ? + {!this.props.chromeHidden && type !== DocumentType.PRES ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}> <EditableView @@ -332,7 +332,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC return ( <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading} style={{ - width: `${100 / ((uniqueHeadings.length + ((this.props.chromeStatus !== 'view-mode' && this.props.chromeStatus) ? 1 : 0)) || 1)}%`, + width: `${100 / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1)}%`, height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, background: this._background }} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 3e6deaf3a..28f4f76be 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -74,7 +74,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const { Document, DataDoc } = this.props; const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? 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 pair.layout && !pair.layout.hidden && (!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 } diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 1f6322997..0702febae 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -39,8 +39,6 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { getAnchor = () => { const anchor = Docs.Create.TextanchorDocument({ title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, - useLinkSmallAnchor: true, - hideLinkButton: true, annotationOn: this.rootDoc }); @@ -232,7 +230,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { } return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu} - style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.layoutDoc._chromeStatus === "enabled" ? 51 : 0}px)` }}> + style={{ width: this.props.PanelWidth(), height: "100%" }}> {this.pivotKeyUI} {this.contents} {!this.props.isSelected() || !doTimeline ? (null) : <> diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 020bb374a..1e693f594 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -73,6 +73,7 @@ export interface CollectionViewProps extends FieldViewProps { // property overrides for child documents children?: never | (() => JSX.Element[]) | React.ReactNode; childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) + childFitWidth?: () => boolean; childOpacity?: () => number; childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children childHideDecorationTitle?: () => boolean; diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index d26f53e28..4005c751e 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -559,8 +559,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - {this.props.Document._chromeStatus ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> - : undefined} + {this.props.Document._chromeHidden ? undefined : <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div>} {!this._showDoc ? (null) : <div className="collectionSchemaView-documentPreview" style={{ diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 2ead98aa4..f333c4077 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -30,10 +30,9 @@ import { DefaultLayerProvider, DefaultStyleProvider, StyleLayers, StyleProp } fr import { CollectionDockingView } from './CollectionDockingView'; import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { CollectionViewType } from './CollectionView'; +import { CollectionViewType, CollectionView } from './CollectionView'; import "./TabDocView.scss"; import React = require("react"); -import Color = require('color'); const _global = (window /* browser */ || global /* node */) as any; interface TabDocViewProps { @@ -336,7 +335,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - <Tooltip style={{ display: this._document?._viewType !== CollectionViewType.Freeform ? "none" : undefined }} key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> + <Tooltip style={{ display: this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform ? "none" : undefined }} 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> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 51cb9387a..a8f5e6dd2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -40,7 +40,6 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo 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. - if (!linkDoc.linkAutoMove) return; const acont = A.rootDoc.type === DocumentType.LINK ? A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; const bcont = B.rootDoc.type === DocumentType.LINK ? B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; const adiv = acont.length ? acont[0] : A.ContentDiv; @@ -60,8 +59,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor2 as Doc)[Id])).lastElement(); if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return; if (!targetAhyperlink) { - linkDoc.anchor1_x = (apt.point.x - aleft) / awidth * 100; - linkDoc.anchor1_y = (apt.point.y - atop) / aheight * 100; + if (linkDoc.linkAutoMove) { + linkDoc.anchor1_x = (apt.point.x - aleft) / awidth * 100; + linkDoc.anchor1_y = (apt.point.y - atop) / aheight * 100; + } } else { const m = targetAhyperlink.getBoundingClientRect(); const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); @@ -69,8 +70,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo linkDoc.anchor1_y = Math.min(1, mp[1] / A.props.PanelHeight()) * 100; } if (!targetBhyperlink) { - linkDoc.anchor2_x = (bpt.point.x - bleft) / bwidth * 100; - linkDoc.anchor2_y = (bpt.point.y - btop) / bheight * 100; + if (linkDoc.linkAutoMove) { + linkDoc.anchor2_x = (bpt.point.x - bleft) / bwidth * 100; + linkDoc.anchor2_y = (bpt.point.y - btop) / bheight * 100; + } } else { const m = targetBhyperlink.getBoundingClientRect(); const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 57dba0f75..f0d99611a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -921,7 +921,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } SelectionManager.DeselectAll(); - if (this.props.Document.scrollHeight) { + if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined) { this.props.focus(doc, options); } else { const xfToCollection = options?.docTransform ?? Transform.Identity(); @@ -942,7 +942,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.setPan(panX, panY, focusSpeed, 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 scale && (this.Document[this.scaleFieldKey] = scale); } - Doc.BrushDoc(doc); const startTime = Date.now(); // focus on this collection within its parent view. the parent view after focusing determines whether to reset the view change within the collection @@ -1202,12 +1201,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } getAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ - title: StrCast(this.layoutDoc._viewType), - useLinkSmallAnchor: true, - hideLinkButton: true, - annotationOn: this.rootDoc - }); + const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc }); const proto = Doc.GetProto(anchor); proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType; proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]); @@ -1245,7 +1239,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.props.renderDepth && this._marqueeRef?.current) { + if (!this.layoutDoc._noAutoscroll && !this.props.renderDepth && this._marqueeRef?.current) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef.current?.getBoundingClientRect(); diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index fc881ca25..3fcb024df 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -144,24 +144,6 @@ border-radius: 3px; z-index: 1000; overflow: hidden; - - .waveform { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - z-index: -1000; - bottom: 0; - pointer-events: none; - div { - height: 100% !important; - width: 100% !important; - } - canvas { - height: 100% !important; - width: 100% !important; - } - } } .audioBox-total-time, diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index ecc93ce67..06a27c22a 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,18 +1,15 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import axios from "axios"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import Waveform from "react-audio-waveform"; import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; -import { List } from "../../../fields/List"; -import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; +import { makeInterface } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils"; +import { emptyFunction, formatTime, Utils } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -23,7 +20,6 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import "./AudioBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; declare class MediaRecorder { constructor(e: any); // whatever MediaRecorder has @@ -36,7 +32,6 @@ const AudioDocument = makeInterface(documentSchema); export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } public static Enabled = false; - public static NUMBER_OF_BUCKETS = 100; static playheadWidth = 30; // width of playhead static heightPercent = 80; // height of timeline in percent of height of audioBox. static Instance: AudioBox; @@ -58,8 +53,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD @observable _position: number = 0; @observable _waveHeight: Opt<number> = this.layoutDoc._height; @observable _paused: boolean = false; - @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); } - set audioState(value) { this.dataDoc.audioState = value; } + @computed get mediaState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.mediaState as (undefined | "recording" | "paused" | "playing"); } + set mediaState(value) { this.dataDoc.mediaState = value; } public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; }); @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); } @@ -89,7 +84,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } getAnchor = () => { - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow" /* audioStart */, "_timecodeToHide" /* audioEnd */, this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc; + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, + "_timecodeToShow" /* audioStart */, "_timecodeToHide" /* audioEnd */, this._ele?.currentTime || + Cast(this.props.Document._currentTimecode, "number", null) || (this.mediaState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) + || this.rootDoc; } componentWillUnmount() { @@ -102,9 +100,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - this.audioState = this.path ? "paused" : undefined; - - //this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); + this.mediaState = this.path ? "paused" : undefined; this._disposers.triggerAudio = reaction( () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined, @@ -131,7 +127,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // for updating the timecode timecodeChanged = () => { const htmlEle = this._ele; - if (this.audioState !== "recording" && htmlEle) { + if (this.mediaState !== "recording" && htmlEle) { htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration); this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => { if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { @@ -145,7 +141,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // pause play back Pause = action(() => { this._ele!.pause(); - this.audioState = "paused"; + this.mediaState = "paused"; }); // play audio for documents created during recording @@ -169,7 +165,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } else if (seekTimeInSeconds <= this._ele.duration) { this._ele.currentTime = seekTimeInSeconds; this._ele.play(); - runInAction(() => this.audioState = "playing"); + runInAction(() => this.mediaState = "playing"); if (endTime !== this.duration) { this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration } @@ -181,7 +177,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // update the recording time updateRecordTime = () => { - if (this.audioState === "recording") { + if (this.mediaState === "recording") { setTimeout(this.updateRecordTime, 30); if (this._paused) { this._pausedTime += (new Date().getTime() - this._recordStart) / 1000; @@ -204,7 +200,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } }; this._recordStart = new Date().getTime(); - runInAction(() => this.audioState = "recording"); + runInAction(() => this.mediaState = "recording"); setTimeout(this.updateRecordTime, 0); this._recorder.start(); setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour @@ -214,8 +210,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -224,7 +220,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._recorder.stop(); this._recorder = undefined; this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; - this.audioState = "paused"; + this.mediaState = "paused"; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); @@ -250,7 +246,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD NumCast(this.props.Document._width), 2 * NumCast(this.props.Document._height)); Doc.GetProto(newDoc).recordingSource = this.dataDoc; Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); - Doc.GetProto(newDoc).audioState = ComputedField.MakeFunction("self.recordingSource.audioState"); + Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); this.props.addDocument?.(newDoc); e.stopPropagation(); } @@ -295,40 +291,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD e.stopPropagation(); } - // returns the audio waveform - @computed get waveform() { - const audioBuckets = Cast(this.dataDoc.audioBuckets, listSpec("number"), []); - !audioBuckets.length && setTimeout(() => this.createWaveformBuckets()); - return <Waveform - color={"darkblue"} - height={this._waveHeight} - barWidth={0.1} - pos={this.duration} - duration={this.duration} - peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined} - progressColor={"blue"} />; - } - - // decodes the audio file into peaks for generating the waveform - createWaveformBuckets = async () => { - axios({ url: this.path, responseType: "arraybuffer" }) - .then(response => (new (window.AudioContext)()).decodeAudioData(response.data, - action(buffer => { - const decodedAudioData = buffer.getChannelData(0); - const bucketDataSize = Math.floor(decodedAudioData.length / AudioBox.NUMBER_OF_BUCKETS); - const brange = Array.from(Array(bucketDataSize)); - this.dataDoc.audioBuckets = new List<number>( - numberRange(AudioBox.NUMBER_OF_BUCKETS).map(i => - brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2)); - })) - ); - } - - playing = () => this.audioState === "playing"; + playing = () => this.mediaState === "playing"; playLink = (link: Doc) => { const stack = this._stackedTimeline.current; if (link.annotationOn === this.rootDoc) { - if (this.layoutDoc.playOnSelect) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link)); + if (!this.layoutDoc.dontAutoPlayFollowedLinks) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link)); else this._ele!.currentTime = this.layoutDoc._currentTimecode = (stack?.anchorStart(link) || 0); } else { @@ -337,7 +304,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2); const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2); if (startTime !== undefined) { - if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; } }); @@ -353,6 +320,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD @computed get renderTimeline() { return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} + mediaPath={this.path} renderDepth={this.props.renderDepth + 1} startTag={"_timecodeToShow" /* audioStart */} endTag={"_timecodeToHide" /* audioEnd */} @@ -385,9 +353,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD {!this.path ? <div className="audiobox-buttons"> <div className="audiobox-dictation" onClick={this.onFile}> - <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + <FontAwesomeIcon style={{ width: "30px", background: !this.layoutDoc.dontAutoPlayFollowedLinks ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> - {this.audioState === "recording" || this.audioState === "paused" ? + {this.mediaState === "recording" || this.mediaState === "paused" ? <div className="recording" onClick={e => e.stopPropagation()}> <div className="buttons" onClick={this.recordClick}> <FontAwesomeIcon icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> @@ -405,11 +373,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="audiobox-controls" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }} > <div className="audiobox-dictation" /> <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} > - <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.audioState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div> + <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.mediaState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.mediaState === "paused" ? "play" : "pause"} size={"1x"} /></div> <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}> - <div className="waveform"> - {this.waveform} - </div> {this.renderTimeline} </div> {this.audio} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index da287649e..724394025 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -5,4 +5,5 @@ touch-action: manipulation; top: 0; left: 0; + pointer-events: none; }
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 58fb005b5..e96fc088e 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -176,7 +176,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF zIndex: this.ZInd, mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, display: this.ZInd === -99 ? "none" : undefined, - pointerEvents: "none" }} > {Doc.UserDoc().renderStyle !== "comic" ? (null) : diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss index da3266dc1..d5f2a7ec7 100644 --- a/src/client/views/nodes/ColorBox.scss +++ b/src/client/views/nodes/ColorBox.scss @@ -6,7 +6,6 @@ transform-origin: top left; .sketch-picker { - margin:auto; div { cursor: crosshair; } diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 61cc8b3d1..59a26f323 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { action } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from 'react-color'; -import { Doc } from "../../../fields/Doc"; +import { Doc, WidthSym, HeightSym } from '../../../fields/Doc'; import { Utils } from "../../../Utils"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; @@ -28,54 +28,45 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument @undoBatch @action static switchColor(color: ColorState) { - Doc.UserDoc().backgroundColor = Utils.colorString(color); + // Doc.UserDoc().backgroundColor = Utils.colorString(color); // bcz: this can't go here ... needs a proper home in the settings panel SetActiveInkColor(color.hex); - if (CurrentUserUtils.SelectedTool === InkTool.None) { - const selected = SelectionManager.Views(); - selected.map(view => { - const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : - view.props.Document.layout instanceof Doc ? view.props.Document.layout : - view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); - if (targetDoc) { - if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot - targetDoc.backgroundColor = Doc.UserDoc().backgroundColor; // bcz: don't know how to change the color of an inline template... - } - else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== "") { - Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + "-color"] = Doc.UserDoc().backgroundColor; - } else { - Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment - } + SelectionManager.Views().map(view => { + const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : + view.props.Document.layout instanceof Doc ? view.props.Document.layout : + view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); + if (targetDoc) { + if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot + targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template... } - }); - } + else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== "") { + Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + "-color"] = color.hex; + } else { + Doc.Layout(view.props.Document)._backgroundColor = color.hex; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment + } + } + }); } - constructor(props: any) { - super(props); - } render() { - const selDoc = SelectionManager.Views()?.[0]?.rootDoc; + const scaling = Math.min(this.layoutDoc.fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]()); return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} onClick={e => { (e.nativeEvent as any).stuff = true; e.stopPropagation(); }} - style={{ width: `${100}%`, height: `${100}%` }} > + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} onClick={e => e.stopPropagation()} + style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }} > - <SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={StrCast(selDoc?._backgroundColor, ActiveInkColor())} /> + <SketchPicker + onChange={c => CurrentUserUtils.SelectedTool === InkTool.None && ColorBox.switchColor(c)} + color={StrCast(SelectionManager.Views()?.[0]?.rootDoc?._backgroundColor, ActiveInkColor())} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', + '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} + /> - <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}> - <div> {ActiveInkWidth() ?? 2}</div> - <input type="range" defaultValue={ActiveInkWidth() ?? 2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + <div style={{ width: this.props.PanelWidth() / scaling, display: "flex", paddingTop: "10px" }}> + <div> {ActiveInkWidth()}</div> + <input type="range" defaultValue={ActiveInkWidth()} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { SetActiveInkWidth(e.target.value); SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeWidth = Number(e.target.value)); }} /> - {/* <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> </div>; } diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 57d1a41b6..a6d07374a 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -31,7 +31,6 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; - links: Doc[]; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -225,7 +224,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } })); - @action clearLinks() { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; @@ -233,15 +231,13 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get filteredLinks() { const results = [] as Doc[]; - Array.from(new Set<Doc>(this.props.links)).forEach(link => { - if (!DocUtils.FilterDocs([link], this.props.View.props.docFilters(), []).length) { - if (DocUtils.FilterDocs([link.anchor2 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - if (DocUtils.FilterDocs([link.anchor1 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - } else results.push(link); + const filters = this.props.View.props.docFilters(); + Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { + if (DocUtils.FilterDocs([link], filters, []).length || + DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || + DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { + results.push(link); + } }); return results; } @@ -296,12 +292,12 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp 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></>}> + <Tooltip title={<div className="dash-tooltip">{title}</div>}> {this.linkButtonInner} </Tooltip> : !DocumentLinksButton.LinkEditorDocView && !this.props.InMenu ? - <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> + <Tooltip title={<div className="dash-tooltip">{title}</div>}> {this.linkButtonInner} </Tooltip> : this.linkButtonInner; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index bbbc6572f..231c9ff38 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -85,6 +85,7 @@ export interface DocComponentView { menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) + playFrom?: (time: number, endTime?: number) => void; } export interface DocumentViewSharedProps { renderDepth: number; @@ -101,6 +102,7 @@ export interface DocumentViewSharedProps { layerProvider: undefined | ((doc: Doc, assign?: boolean) => boolean); styleProvider: Opt<StyleProviderFunc>; focus: DocFocusFunc; + fitWidth?: () => boolean; docFilters: () => string[]; docRangeFilters: () => string[]; searchFilterDocs: () => Doc[]; @@ -158,7 +160,8 @@ export interface DocumentViewInternalProps extends DocumentViewProps { @observer export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps, Document>(Document) { @observable _animateScalingTo = 0; - @observable _audioState = 0; + @observable _mediaState = 0; + @observable _pendingDoubleClick = false; private _downX: number = 0; private _downY: number = 0; private _firstX: number = -1; @@ -171,7 +174,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _dropDisposer?: DragManager.DragDropDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - _componentView: Opt<DocComponentView>; + _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class private get topMost() { return this.props.renderDepth === 0; } private get active() { return this.props.isSelected(true) || this.props.parentActive(true); } @@ -292,7 +295,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e.cancelBubble && this.active) { this.removeMoveListeners(); } - else if (!e.cancelBubble && (this.props.isSelected(true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + else if (!e.cancelBubble && (this.active || 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)) { if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) { @@ -342,7 +345,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc)); } layoutDoc._width = actualdW; - if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; else layoutDoc._height = actualdH; } else { @@ -350,7 +353,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc)); } layoutDoc._height = actualdH; - if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; else layoutDoc._width = actualdW; } } else { @@ -483,10 +486,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 } else { - const ctrlPressed = e.ctrlKey || e.shiftKey; - if (this.props.Document.type === DocumentType.WEB) { - this._timeout = setTimeout(() => { this._timeout = undefined; this.props.select(ctrlPressed); }, 350); - } else this.props.select(ctrlPressed); + runInAction(() => this._pendingDoubleClick = true); + this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350); + this.props.select(e.ctrlKey || e.shiftKey); } preventDefault = false; } @@ -530,7 +532,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 && (this.props.isSelected(true) || this.props.parentActive(true) || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + else if (!e.cancelBubble && (this.active || 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); @@ -560,9 +562,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = !this.Document.isLinkButton; - setPushpin && (this.Document.isPushpin = this.Document.isLinkButton); - if (this.Document.isLinkButton && !this.onClickHandler) { + this.Document._isLinkButton = !this.Document._isLinkButton; + setPushpin && (this.Document.isPushpin = this.Document._isLinkButton); + if (this.Document._isLinkButton && !this.onClickHandler) { this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; } else { @@ -572,13 +574,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action toggleTargetOnClick = (): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = true; + this.Document._isLinkButton = true; this.Document.isPushpin = true; } @undoBatch @action followLinkOnClick = (location: Opt<string>, zoom: boolean,): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = true; + this.Document._isLinkButton = true; this.Document.isPushpin = false; this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; @@ -586,14 +588,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action selectOnClick = (): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = false; + this.Document._isLinkButton = false; this.Document.isPushpin = false; this.Document.onClick = this.layoutDoc.onClick = undefined; } @undoBatch noOnClick = (): void => { this.Document.ignoreClick = false; - this.Document.isLinkButton = false; + this.Document._isLinkButton = false; } @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); @@ -632,7 +634,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } this.Document.followLinkLocation = "inPlace"; this.Document.followLinkZoom = true; - this.Document.isLinkButton = true; + this.Document._isLinkButton = true; } @action @@ -723,7 +725,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); if (!Doc.UserDoc().noviceMode) { moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: `${this.Document._chromeStatus ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus ? undefined : "enabled"), icon: "project-diagram" }); + moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" }); if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); @@ -772,7 +774,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps style={{ height: 25, position: "absolute", top: 10, left: 10 }} > <FontAwesomeIcon className="documentView-audioFont" - style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} + style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._mediaState] }} icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" /> </div>; return <div className="documentView-contentsView" @@ -793,9 +795,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClick={this.onClickFunc} focus={this.focus} layoutKey={this.finalLayoutKey} /> - {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} + {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} {this.hideLinkButton ? (null) : - <DocumentLinksButton View={this.props.DocumentView()} links={this.allLinks} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />} + <DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />} {audioView} </div>; @@ -816,11 +818,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } @computed get directLinks() { TraceMobx(); return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } - @computed get allAnchors() { + @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); if (this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null); - // need to use allLinks for RTF since embedded linked text anchors are not rendered with DocumentViews. All other documents render their anchors with nested DocumentViews so we just need to render the directLinks here const filtered = DocUtils.FilterDocs(this.rootDoc.type === DocumentType.RTF ? this.allLinks : this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden); return filtered.map((link, i) => @@ -841,7 +842,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onPointerEnter = () => { const self = this; const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]); - if (audioAnnos && audioAnnos.length && this._audioState === 0) { + if (audioAnnos && audioAnnos.length && this._mediaState === 0) { const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; anno.data instanceof AudioField && new Howl({ src: [anno.data.url.href], @@ -850,10 +851,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps loop: false, volume: 0.5, onend: function () { - runInAction(() => self._audioState = 0); + runInAction(() => self._mediaState = 0); } }); - this._audioState = 1; + this._mediaState = 1; } } recordAudioAnnotation = () => { @@ -878,11 +879,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } }; - runInAction(() => self._audioState = 2); + runInAction(() => self._mediaState = 2); recorder.start(); setTimeout(() => { recorder.stop(); - runInAction(() => self._audioState = 0); + runInAction(() => self._mediaState = 0); gumStream.getAudioTracks()[0].stop(); }, 5000); }); @@ -1002,6 +1003,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { get ComponentView() { return this.docView?._componentView; } get allLinks() { return this.docView?.allLinks || []; } get LayoutFieldKey() { return this.docView?.LayoutFieldKey || "layout"; } + get fitWidth() { return this.props.fitWidth?.() || this.layoutDoc.fitWidth; } @computed get docViewPath() { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } @@ -1013,13 +1015,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); } - @computed get shouldNotScale() { return (this.layoutDoc._fitWidth && !this.nativeWidth) || [CollectionViewType.Docking, CollectionViewType.Tree].includes(this.Document._viewType as any); } + @computed get shouldNotScale() { return (this.fitWidth && !this.nativeWidth) || [CollectionViewType.Docking, CollectionViewType.Tree].includes(this.Document._viewType as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); } @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); } @computed get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.layoutDoc._fitWidth || this.props.PanelHeight() / this.effectiveNativeHeight > this.props.PanelWidth() / this.effectiveNativeWidth) { + if (this.fitWidth || this.props.PanelHeight() / this.effectiveNativeHeight > this.props.PanelWidth() / this.effectiveNativeWidth) { return Math.max(minTextScale, this.props.PanelWidth() / this.effectiveNativeWidth); // width-limited or fitWidth } return Math.max(minTextScale, this.props.PanelHeight() / this.effectiveNativeHeight); // height-limited or unscaled @@ -1035,7 +1037,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; } @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; } @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; } - @computed get centeringY() { return this.props.Document._fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } + @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); contentsActive = () => this.docView?.contentsActive(); @@ -1108,16 +1110,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { render() { TraceMobx(); - const xshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; - const yshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; + const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); + const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); return (<div className="contentFittingDocumentView"> {!this.props.Document || !this.props.PanelWidth() ? (null) : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} style={{ position: this.props.Document.isInkMask ? "absolute" : undefined, transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: xshift ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, - height: yshift ?? (this.props.Document._fitWidth ? `${this.panelHeight}px` : + width: xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, + height: yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 465c18309..97f53aac0 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -73,28 +73,7 @@ export class FieldView extends React.Component<FieldViewProps> { return <p>{field.date.toLocaleString()}</p>; } else if (field instanceof Doc) { - return <p><b>{field.title && field.title.toString()}</b></p>; - //return <p><b>{field.title + " : id= " + field[Id]}</b></p>; - // let returnHundred = () => 100; - // return ( - // <DocumentContentsView Document={field} - // addDocument={undefined} - // addDocTab={this.props.addDocTab} - // removeDocument={undefined} - // ScreenToLocalTransform={Transform.Identity} - // ContentScaling={returnOne} - // PanelWidth={returnHundred} - // PanelHeight={returnHundred} - // renderDepth={0} //TODO Why is renderDepth reset? - // focus={emptyFunction} - // isSelected={this.props.isSelected} - // select={returnFalse} - // layoutKey={"layout"} - // ContainingCollectionView={this.props.ContainingCollectionView} - // parentActive={this.props.active} - // whenActiveChanged={this.props.whenActiveChanged} - // bringToFront={emptyFunction} /> - // ); + return <p><b>{field.title?.toString()}</b></p>; } else if (field instanceof List) { return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : ""} </div>; diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index e8bec9676..4be7d1c37 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -37,11 +37,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps, Equati () => this.createGraph()); } getAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ - useLinkSmallAnchor: true, - hideLinkButton: true, - annotationOn: this.rootDoc - }); + const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc }); anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); return anchor; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 383650e89..111509fdb 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -163,7 +163,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } getTemplate = async () => { - const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template" }); + const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template", _chromeHidden: true }); parent._columnWidth = 100; for (const row of this.rows.filter(row => row.isChecked)) { await this.createTemplateField(parent, row); diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 392565402..3d72d047e 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -93,7 +93,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch } openLinkTargetOnRight = (e: React.MouseEvent) => { const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); - alias.isLinkButton = undefined; + alias._isLinkButton = undefined; alias._layerTags = undefined; alias.layoutKey = "layout"; this.props.addDocTab(alias, "add:right"); diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 564873cf5..0f46da294 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -18,11 +18,12 @@ top: 0; left: 0; - .pdfBox-overlayButton-sidebar { + .pdfBox-sidebarBtn { background: #121721; height: 25px; width: 25px; right: 0; + color: white; display: flex; position: absolute; align-items: center; @@ -39,8 +40,7 @@ left: 5px; top: 5px; - .pdfBox-overlayButton-fwd, - .pdfBox-overlayButton-back { + .pdfBox-fwdBtn, .pdfBox-backBtn { background: #121721; height: 25px; width: 25px; @@ -63,6 +63,9 @@ padding: 0; position: absolute; pointer-events: all; + color: white; + bottom: 0; + right: 0; .pdfBox-overlayButton-arrow { width: 0; @@ -89,6 +92,7 @@ .pdfBox-nextIcon, .pdfBox-prevIcon { background: #121721; + color: white; height: 20px; width: 25px; display: flex; @@ -189,26 +193,7 @@ } } - .pdfBox-tagList { - display: flex; - flex-direction: row; - overflow: auto; - flex-flow: row; - flex-wrap: wrap; - .pdfBox-filterTag, .pdfBox-filterTag-active { - font-weight: bold; - padding-left: 6; - padding-right: 6; - box-shadow: black 1px 1px 4px; - border-radius: 5; - margin: 2; - height: 20; - background-color: lightgrey; - } - .pdfBox-filterTag-active { - background-color: white; - } - } + .pdfBox-title-outer { width: 100%; height: 100%; @@ -330,6 +315,7 @@ font-size: 30px; width: 50px; height: 50px; + color: white; } .pdfBox .pdfBox-ui .pdfBox-nextIcon, diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 0dbe0c917..f27a34e36 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -3,33 +3,26 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import { Doc, DocListCast, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; +import { Doc, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from '../../../fields/documentSchemas'; -import { Id } from '../../../fields/FieldSymbols'; import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { PdfField } from "../../../fields/URLField"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, OmitKeys, returnOne, returnTrue, returnZero, Utils } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { Utils } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch } from '../../util/UndoManager'; import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView'; -import { CollectionStackingView } from '../collections/CollectionStackingView'; -import { CollectionViewType } from '../collections/CollectionView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { PDFViewer } from "../pdf/PDFViewer"; -import { SearchBox } from '../search/SearchBox'; -import { StyleProp } from '../StyleProvider'; +import { SidebarAnnos } from '../SidebarAnnos'; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); -import { DocFocusOptions } from './DocumentView'; -import { List } from '../../../fields/List'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -38,32 +31,30 @@ const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } 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 _displayPdfLive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title + private _initialScrollTarget: Opt<Doc>; private _pdfViewer: PDFViewer | undefined; private _searchRef = React.createRef<HTMLInputElement>(); private _selectReactionDisposer: IReactionDisposer | undefined; + private _sidebarRef = React.createRef<SidebarAnnos>(); @observable private _searching: boolean = false; @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; @observable private _pageControls = false; + @computed get pdfUrl() { return Cast(this.dataDoc[this.props.fieldKey], PdfField); } + constructor(props: any) { super(props); - this._initialScale = this.props.ScreenToLocalTransform().Scale; 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)); + if (this.pdfUrl) { + if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href)); + else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf)); } const backup = "oldPath"; - const { Document } = this.props; - const pdf = Cast(this.dataDoc[this.props.fieldKey], PdfField); - const href = pdf?.url?.href; + const href = this.pdfUrl?.url.href; if (href) { const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g; const matches = pathCorrectionTest.exec(href); @@ -75,7 +66,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum //console.log(properPath); if (!properPath.includes(href)) { console.log(`The two (url and proper path) were not equal`); - const proto = Doc.GetProto(Document); + const proto = Doc.GetProto(this.props.Document); proto[this.props.fieldKey] = new PdfField(properPath); proto[backup] = href; } else { @@ -87,37 +78,30 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum } } - initialScrollTarget: Opt<Doc>; + componentWillUnmount() { this._selectReactionDisposer?.(); } + componentDidMount() { + this.props.setContentView?.(this); + this._selectReactionDisposer = reaction(() => this.props.isSelected(), + () => { + document.removeEventListener("keydown", this.onKeyDown); + this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown); + }, { fireImmediately: true }); + } + scrollFocus = (doc: Doc, smooth: boolean) => { - if (DocListCast(this.rootDoc[this.sidebarKey()]).includes(doc)) { - if (this.layoutDoc["_" + this.sidebarKey() + "-docFilters"]) { - this.layoutDoc["_" + this.sidebarKey() + "-docFilters"] = new List<string>(); - return 1; - } - } - this.initialScrollTarget = doc; + if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; + this._initialScrollTarget = doc; return this._pdfViewer?.scrollFocus(doc, smooth); } getAnchor = () => { const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), - useLinkSmallAnchor: true, - hideLinkButton: true, annotationOn: this.rootDoc, y: NumCast(this.layoutDoc._scrollTop), }); this.addDocument(anchor); return anchor; } - componentWillUnmount() { this._selectReactionDisposer?.(); } - componentDidMount() { - this.props.setContentView?.(this); - this._selectReactionDisposer = reaction(() => this.props.isSelected(), - () => { - document.removeEventListener("keydown", this.onKeyDown); - this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown); - }, { fireImmediately: true }); - } @action loaded = (nw: number, nh: number, np: number) => { @@ -127,89 +111,25 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1); !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } - sidebarKey = () => this.fieldKey + "-sidebar"; - sidebarFiltersHeight = () => 50; - sidebarTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.dataDoc), 0).scale(this.props.scaling?.() || 1); - sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); - sidebarHeight = () => this.props.PanelHeight() - this.sidebarFiltersHeight() - 20; - sidebarAddDocument = (doc: Doc | Doc[]) => this.addDocument(doc, this.sidebarKey()); - sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.sidebarKey()); - sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.sidebarKey()); - sidebarDocFilters = () => [...StrListCast(this.layoutDoc._docFilters), ...StrListCast(this.layoutDoc[this.sidebarKey() + "-docFilters"])]; - @computed get allTags() { - const keys = new Set<string>(); - DocListCast(this.rootDoc[this.sidebarKey()]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys.keys()).filter(key => key[0]).filter(key => !key.startsWith("_") && (key[0] === "#" || key[0] === key[0].toUpperCase())).sort(); - } - renderTag = (tag: string) => { - const active = StrListCast(this.rootDoc[this.sidebarKey() + "-docFilters"]).includes(`${tag}:${tag}:check`); - return <div key={tag} className={`pdfbox-filterTag${active ? "-active" : ""}`} - onClick={e => Doc.setDocFilter(this.rootDoc, tag, tag, "check", true, this.sidebarKey())}> - {tag} - </div>; - } - @computed get sidebarOverlay() { - return !this.layoutDoc._showSidebar ? (null) : - <div style={{ - position: "absolute", pointerEvents: this.active() ? "all" : undefined, top: 0, right: 0, - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor), - width: `${this.sidebarWidth()}px`, - height: "100%" - }}> - <div style={{ width: "100%", height: this.sidebarHeight(), position: "relative" }}> - <CollectionStackingView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - NativeWidth={returnZero} - NativeHeight={returnZero} - PanelHeight={this.sidebarHeight} - PanelWidth={this.sidebarWidth} - xMargin={0} - yMargin={0} - docFilters={this.sidebarDocFilters} - chromeStatus={"enabled"} - scaleField={this.sidebarKey() + "-scale"} - isAnnotationOverlay={false} - select={emptyFunction} - active={this.annotationsActive} - scaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - childHideDecorationTitle={returnTrue} - removeDocument={this.sidebarRemDocument} - moveDocument={this.sidebarMoveDocument} - addDocument={this.sidebarAddDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarTransform} - renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} - fieldKey={this.sidebarKey()} - pointerEvents={"all"} - /> - </div> - <div className="pdfBox-tagList" style={{ height: this.sidebarFiltersHeight(), width: this.sidebarWidth() }}> - {this.allTags.map(tag => this.renderTag(tag))} - </div> - </div>; - } - public search = (string: string, fwd: boolean) => { this._pdfViewer?.search(string, fwd); }; - public prevAnnotation = () => { this._pdfViewer?.prevAnnotation(); }; - public nextAnnotation = () => { this._pdfViewer?.nextAnnotation(); }; + public search = (string: string, fwd: boolean) => this._pdfViewer?.search(string, fwd); + public prevAnnotation = () => this._pdfViewer?.prevAnnotation(); + public nextAnnotation = () => this._pdfViewer?.nextAnnotation(); public backPage = () => { this.Document._curPage = (this.Document._curPage || 1) - 1; return true; }; public forwardPage = () => { this.Document._curPage = (this.Document._curPage || 1) + 1; return true; }; - public gotoPage = (p: number) => { this.Document._curPage = p; }; + public gotoPage = (p: number) => this.Document._curPage = p; @undoBatch onKeyDown = action((e: KeyboardEvent) => { let processed = false; - if (e.key === "f" && e.ctrlKey) { - this._searching = true; - setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100); - processed = true; - } - if (e.key === "PageDown") processed = this.forwardPage(); - if (e.key === "PageUp") processed = this.backPage(); - if (e.target instanceof HTMLInputElement || this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { - if (e.key === "ArrowDown" || e.key === "ArrowRight") processed = this.forwardPage(); - if (e.key === "ArrowUp" || e.key === "ArrowLeft") processed = this.backPage(); + switch (e.key) { + case "f": if (e.ctrlKey) { + setTimeout(() => this._searchRef.current?.focus(), 100); + this._searching = processed = true; + } + break; + case "PageDown": processed = this.forwardPage(); break; + case "PageUp": processed = this.backPage(); break; } if (processed) { e.stopImmediatePropagation(); @@ -219,97 +139,87 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; - if (this.initialScrollTarget) { - this.scrollFocus(this.initialScrollTarget, false); - this.initialScrollTarget = undefined; + if (this._initialScrollTarget) { + this.scrollFocus(this._initialScrollTarget, false); + this._initialScrollTarget = undefined; } } searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; - toggleSidebar = () => { - if (this.layoutDoc.nativeWidth === this.layoutDoc[this.fieldKey + "-nativeWidth"]) { - this.layoutDoc.nativeWidth = 250 + NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } else { - this.layoutDoc.nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } - this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])); + + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + return this.addDocument(doc, sidebarKey); } + toggleSidebar = action(() => { + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth; + const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); + this.layoutDoc.nativeWidth = nativeWidth * ratio; + this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; + this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + }); settingsPanel() { const pageBtns = <> - <button className="pdfBox-overlayButton-back" key="back" title="Page Back" - onPointerDown={e => e.stopPropagation()} onClick={e => this.backPage()} > + <button className="pdfBox-backBtn" key="back" title="Page Back" + onPointerDown={e => e.stopPropagation()} onClick={this.backPage} > <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" /> </button> - <button className="pdfBox-overlayButton-fwd" key="fwd" title="Page Forward" - onPointerDown={e => e.stopPropagation()} onClick={e => this.forwardPage()} > + <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward" + onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage} > <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> </button> </>; const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; const curPage = this.Document._curPage || 1; return !this.active() ? (null) : - (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} + <div className="pdfBox-ui" onKeyDown={e => [KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> - <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> + <div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> - <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> + <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} + onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> - <FontAwesomeIcon icon="search" size="sm" color="white" /></button> - <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={this.prevAnnotation} > - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="lg" /> + <FontAwesomeIcon icon="search" size="sm" /> + </button> + <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation} > + <FontAwesomeIcon icon={"arrow-up"} size="lg" /> </button> <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation} > - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="lg" /> + <FontAwesomeIcon icon={"arrow-down"} size="lg" /> </button> </div> - <button className="pdfBox-overlayButton" key="search" onClick={action(() => { - this._searching = !this._searching; - this.search("mxytzlaf", true); - })} title={searchTitle} style={{ bottom: 0, right: 0 }}> - <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> + <button className="pdfBox-overlayButton" title={searchTitle} + onClick={action(() => { this._searching = !this._searching; this.search("mxytzlaf", true); })} > + <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} /> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> - <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="lg" /></div> + <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" /> + </div> </button> <div className="pdfBox-pageNums"> - <input value={curPage} + <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} onChange={e => this.Document._curPage = Number(e.currentTarget.value)} - style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} </div> - <button className="pdfBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" style={{ right: this.sidebarWidth() + 7 }} + <button className="pdfBox-sidebarBtn" title="Toggle Sidebar" + style={{ right: this.sidebarWidth() + 7, display: !this.active() ? "none" : undefined }} onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} > - <FontAwesomeIcon style={{ color: "white" }} icon={"chevron-left"} size="sm" /> + <FontAwesomeIcon icon={"chevron-left"} size="sm" /> </button> - </div>); + </div>; } + sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); specificContextMenu = (e: React.MouseEvent): void => { - const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); const funcs: ContextMenuProps[] = []; - pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" }); + funcs.push({ description: "Copy path", event: () => this.pdfUrl && Utils.CopyText(this.pdfUrl.url.pathname), icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Annotation View ", event: () => this.Document._showSidebar = !this.Document._showSidebar, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } - anchorMenuClick = (anchor: Doc) => { - this.Document._showSidebar = true; - const startup = StrListCast(this.rootDoc.docFilters).map(filter => filter.split(":")[0]).join(" "); - const target = Docs.Create.TextDocument(startup, { - title: "anno", - annotationOn: this.rootDoc, _width: 200, _height: 50, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily) - }); - FormattedTextBox.SelectOnLoad = target[Id]; - FormattedTextBox.DontSelectInitialText = true; - this.allTags.map(tag => target[tag] = tag); - DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup", "annotation"); - this.sidebarAddDocument(target); - } - @computed get renderTitleBox() { const classname = "pdfBox" + (this.active() ? "-interactive" : ""); return <div className={classname} > @@ -318,17 +228,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum </div> </div>; } + @computed get renderPdfView() { TraceMobx(); - 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 }}> + 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" /> <PDFViewer {...this.props} pdf={this._pdf!} - url={pdfUrl!.url.pathname} + url={this.pdfUrl!.url.pathname} active={this.active} - anchorMenuClick={this.anchorMenuClick} + anchorMenuClick={this._sidebarRef.current?.anchorMenuClick} loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} setPdfViewer={this.setPdfViewer} addDocument={this.addDocument} @@ -337,7 +250,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum ContentScaling={this.props.scaling} sidebarWidth={this.sidebarWidth} /> - {this.sidebarOverlay} + <SidebarAnnos ref={this._sidebarRef} + {...this.props} + annotationsActive={this.annotationsActive} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + active={this.active} + /> {this.settingsPanel()} </div>; } @@ -346,19 +269,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum static pdfpromise = new Map<string, Pdfjs.PDFPromise<Pdfjs.PDFDocumentProxy>>(); render() { TraceMobx(); - if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0)) { - this._displayPdfLive = true; - } - if (this._displayPdfLive) { - if (this._pdf) return this.renderPdfView; + 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))); - } + const href = this.pdfUrl?.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.tsx b/src/client/views/nodes/PresBox.tsx index 3c1fda465..2aba461e0 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -13,7 +13,7 @@ import { PrefetchProxy } from "../../../fields/Proxy"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { returnFalse, returnOne } from "../../../Utils"; +import { returnFalse, returnOne, returnTrue, emptyFunction } from '../../../Utils'; import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -195,7 +195,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> componentDidMount() { this.rootDoc.presBox = this.rootDoc; this.rootDoc._forceRenderEngine = "timeline"; - this.rootDoc._replacedChrome = "replaced"; this.layoutDoc.presStatus = PresStatus.Edit; this.layoutDoc._gridGap = 0; this.layoutDoc._yMargin = 0; @@ -231,17 +230,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played startTempMedia = (targetDoc: Doc, activeItem: Doc) => { const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime); - if (targetDoc.type === DocumentType.AUDIO) { - if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._triggerAudio = NumCast(activeItem.presStartTime); - this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc]; - } else if (targetDoc.type === DocumentType.VID) { - if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._triggerVideoStop = true; - setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10); - setTimeout(() => targetDoc._triggerVideo = true, 20); - this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc]; + if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) { + const targMedia = DocumentManager.Instance.getDocumentView(targetDoc); + targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.presStartTime), NumCast(activeItem.presStartTime) + duration); } + // if (targetDoc.type === DocumentType.AUDIO) { + // if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); + // targetDoc._triggerAudio = NumCast(activeItem.presStartTime); + // this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc]; + // } else if (targetDoc.type === DocumentType.VID) { + // targetDoc._triggerVideoStop = true; + // setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10); + // setTimeout(() => targetDoc._triggerVideo = true, 20); + // this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc]; + // } } stopTempMedia = (targetDoc: Doc) => { @@ -424,7 +426,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // this still needs some fixing setTimeout(resetSelection, 500); if (doc !== targetDoc) { - setTimeout(() => finished?.(), 100); /// give it some time to create the targetDoc if we're opening up its context + setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context } else { finished?.(); } @@ -2262,7 +2264,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; return ( - <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeStatus ? "none" : undefined }}> + <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? "none" : undefined }}> {isMini ? (null) : <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }} onPointerDown={e => e.stopPropagation()} @@ -2449,6 +2451,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> PanelHeight={this.panelHeight} childIgnoreNativeSize={true} moveDocument={returnFalse} + childFitWidth={returnTrue} childOpacity={returnOne} childLayoutTemplate={this.childLayoutTemplate} filterAddDocument={this.addDocumentFilter} diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss index 141960f60..ab54cf526 100644 --- a/src/client/views/nodes/ScreenshotBox.scss +++ b/src/client/views/nodes/ScreenshotBox.scss @@ -26,7 +26,7 @@ position: absolute; right: 25; top: 0; - width:50; + width:25; height: 25; .screenshotBox-snapshot{ color : white; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index a14d8ccae..c00c79eb9 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,40 +1,59 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import * as rp from 'request-promise'; +import { DateField } from "../../../fields/DateField"; import { Doc, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; +import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; -import { listSpec, makeInterface } from "../../../fields/Schema"; +import { makeInterface } from "../../../fields/Schema"; +import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; -import { VideoField } from "../../../fields/URLField"; -import { emptyFunction, returnFalse, returnOne, returnZero, Utils, OmitKeys } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; +import { AudioField, VideoField } from "../../../fields/URLField"; +import { emptyFunction, OmitKeys, returnFalse, returnOne, Utils } from "../../../Utils"; +import { DocUtils } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Networking } from "../../Network"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { ViewBoxBaseComponent } from "../DocComponent"; +import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -const path = require('path'); +import { VideoBox } from "./VideoBox"; +import { TraceMobx } from "../../../fields/util"; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +declare class MediaRecorder { + constructor(e: any, options?: any); // whatever MediaRecorder has +} type ScreenshotDocument = makeInterface<[typeof documentSchema]>; const ScreenshotDocument = makeInterface(documentSchema); @observer -export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) { - private _reactionDisposer?: IReactionDisposer; - private _videoRef: HTMLVideoElement | null = null; +export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } + private _videoRef = React.createRef<HTMLVideoElement>(); + private _audioRec: any; + private _videoRec: any; + @observable _screenCapture = false; + @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } - public get player(): HTMLVideoElement | null { - return this._videoRef; + constructor(props: any) { + super(props); + this.setupDictation(); + } + getAnchor = () => { + const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide", + startTime, startTime === undefined ? undefined : startTime + 3) + || this.rootDoc; } videoLoad = () => { - const aspect = this.player!.videoWidth / this.player!.videoHeight; + const aspect = this._videoRef.current!.videoWidth / this._videoRef.current!.videoHeight; const nativeWidth = Doc.NativeWidth(this.layoutDoc); const nativeHeight = Doc.NativeHeight(this.layoutDoc); if (!nativeWidth || !nativeHeight) { @@ -44,95 +63,24 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh } } - @action public Snapshot() { - const width = NumCast(this.layoutDoc._width); - const height = NumCast(this.layoutDoc._height); - const canvas = document.createElement('canvas'); - canvas.width = 640; - 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); - ctx.fillStyle = "blue"; - ctx.fill(); - this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); - } - - if (this._videoRef) { - //convert to desired file format - const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' - // if you want to preview the captured image, - const filename = path.basename(encodeURIComponent("screenshot" + Utils.GenerateGuid().replace(/\..*$/, "").replace(" ", "_"))); - ScreenshotBox.convertDataUri(dataUrl, filename).then(returnedFilename => { - setTimeout(() => { - if (returnedFilename) { - const imageSummary = Docs.Create.ImageDocument(Utils.prepend(returnedFilename), { - x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), - _width: 150, _height: height / width * 150, title: "--screenshot--" - }); - if (!this.props.addDocument || this.props.addDocument === returnFalse) { - const spt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - imageSummary.x = spt[0]; - imageSummary.y = spt[1]; - Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); - } else { - this.props.addDocument?.(imageSummary); - } - } - }, 500); - }); - } - } - componentDidMount() { + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; + this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. } - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - } - - @action - setVideoRef = (vref: HTMLVideoElement | null) => { - this._videoRef = vref; + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); } - public static async convertDataUri(imageUri: string, returnedFilename: string) { - try { - const posting = Utils.prepend("/uploadURI"); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log("ScreenShotBox:" + e); - } - } - @observable _screenCapture = false; specificContextMenu = (e: React.MouseEvent): void => { - const field = Cast(this.dataDoc[this.fieldKey], VideoField); - if (field) { - const url = field.url.href; - const subitems: ContextMenuProps[] = []; - subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); - subitems.push({ - description: "Screen Capture", event: (async () => { - runInAction(() => this._screenCapture = !this._screenCapture); - this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); - }), icon: "expand-arrows-alt" - }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); - } + const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }]; + ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" }); } @computed get content() { const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; - const style = "videoBox-content" + interactive; - return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} + return <video className={"videoBox-content" + interactive} key="video" ref={this._videoRef} + autoPlay={this._screenCapture} style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} onCanPlay={this.videoLoad} controls={true} @@ -144,53 +92,114 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh toggleRecording = action(async () => { this._screenCapture = !this._screenCapture; - this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + if (this._screenCapture) { + this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); + const aud_chunks: any = []; + this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); + this._audioRec.onstop = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); + if (!(result instanceof Error)) { + this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client)); + } + }; + this._videoRef.current!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + this._videoRec = new MediaRecorder(this._videoRef.current!.srcObject); + const vid_chunks: any = []; + this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date()); + this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); + this._videoRec.onstop = async (e: any) => { + const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); + const [{ result }] = await Networking.UploadFilesToServer(file); + this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000; + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + this.dataDoc.type = DocumentType.VID; + this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; + this.layoutDoc._fitWidth = undefined; + this.dataDoc[this.props.fieldKey] = new VideoField(Utils.prepend(result.accessPaths.agnostic.client)); + } else alert("video conversion failed"); + }; + this._audioRec.start(); + this._videoRec.start(); + this.dataDoc.mediaState = "recording"; + DocUtils.ActiveRecordings.push(this); + } else { + this._audioRec.stop(); + this._videoRec.stop(); + this.dataDoc.mediaState = "paused"; + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + } }); - private get uIButtons() { - return (<div className="screenshotBox-uiButtons"> - <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > - <FontAwesomeIcon icon="file" size="lg" /> - </div>, - <div className="screenshotBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > - <FontAwesomeIcon icon="camera" size="lg" /> - </div> - </div>); - } - - onSnapshot = (e: React.PointerEvent) => { - this.Snapshot(); - e.stopPropagation(); - e.preventDefault(); + setupDictation = () => { + if (this.dataDoc[this.fieldKey + "-dictation"]) return; + const dictationText = CurrentUserUtils.GetNewTextDoc("dictation", + NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, + NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); + dictationText._autoHeight = false; + const dictationTextProto = Doc.GetProto(dictationText); + dictationTextProto.recordingSource = this.dataDoc; + dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); + dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); + this.dataDoc[this.fieldKey + "-dictation"] = dictationText; } - - contentFunc = () => [this.content]; + videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth(); + formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); render() { - return (<div className="videoBox" onContextMenu={this.specificContextMenu} - style={{ width: `${100}%`, height: `${100}%` }} > + TraceMobx(); + return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} > <div className="videoBox-viewer" > - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.props.PanelWidth} - focus={this.props.focus} - isSelected={this.props.isSelected} - isAnnotationOverlay={true} - select={emptyFunction} - active={returnFalse} - scaling={returnOne} - whenActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - {this.contentFunc} - </CollectionFreeFormView> + <div style={{ position: "relative", height: this.videoPanelHeight() }}> + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + PanelHeight={this.videoPanelHeight} + PanelWidth={this.props.PanelWidth} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + active={returnFalse} + scaling={returnOne} + whenActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + {this.contentFunc} + </CollectionFreeFormView></div> + <div style={{ position: "relative", height: this.formattedPanelHeight() }}> + <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + Document={this.dataDoc[this.fieldKey + "-dictation"]} + fieldKey={"text"} + PanelHeight={this.formattedPanelHeight} + PanelWidth={this.props.PanelWidth} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + active={returnFalse} + scaling={returnOne} + xMargin={25} + yMargin={10} + whenActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + </FormattedTextBox></div> </div> - {this.props.isSelected() ? this.uIButtons : (null)} - </div >); + {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons"> + <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > + <FontAwesomeIcon icon="file" size="lg" /> + </div> + </div>} + </div >; } }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 575fbcf2e..efcddd7b3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,18 +1,18 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, ObservableMap } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, StrListCast } from "../../../fields/Doc"; +import { Doc, DocListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { VideoField } from "../../../fields/URLField"; +import { AudioField, nullAudio, VideoField } from "../../../fields/URLField"; import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; @@ -24,10 +24,8 @@ import { DocumentDecorations } from "../DocumentDecorations"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; import "./VideoBox.scss"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; const path = require('path'); type VideoDocument = makeInterface<[typeof documentSchema]>; @@ -72,8 +70,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD getAnchor = () => { const timecode = Cast(this.layoutDoc._currentTimecode, "number", null); - const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc; - return anchor; + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc; } choosePath(url: string) { @@ -85,13 +82,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD 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; + if (Number.isFinite(this.player!.duration)) { + this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration; + } } @action public Play = (update: boolean = true) => { this._playing = true; try { + this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime); update && this.player?.play(); + update && this._audioPlayer?.play(); update && this._youtubePlayer?.playVideo(); this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); } catch (e) { @@ -107,12 +108,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD console.log("Video Seek Exception:", e); } this.player && (this.player.currentTime = time); + this._audioPlayer && (this._audioPlayer.currentTime = time); } @action public Pause = (update: boolean = true) => { this._playing = false; try { update && this.player?.pause(); + update && this._audioPlayer?.pause(); update && this._youtubePlayer?.pauseVideo(); this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true); @@ -152,8 +155,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const b = Docs.Create.LabelDocument({ x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 1), _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(), + _isLinkButton: true }); - b.isLinkButton = true; this.props.addDocument?.(b); DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); Networking.PostToServer("/youtubeScreenshot", { @@ -179,11 +182,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private createRealSummaryLink = (relative: string) => { const url = this.choosePath(Utils.prepend(relative)); - const width = this.layoutDoc._width || 0; + const width = this.layoutDoc._width || 1; const height = this.layoutDoc._height || 0; const imageSummary = Docs.Create.ImageDocument(url, { _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), - x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), isLinkButton: true, + x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), _isLinkButton: true, _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-" }); Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc)); @@ -286,12 +289,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }), icon: "expand-arrows-alt" }); - subitems.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - subitems.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } + // returns the path of the audio file + @computed get audiopath() { + const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null); + const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null); + return field?.url.href ?? vfield?.url.href ?? ""; + } + // ref for updating time + _audioPlayer: HTMLAudioElement | null = null; + setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e; @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; @@ -308,8 +320,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD onPause={() => this.Pause()} onClick={e => e.preventDefault()}> <source src={field.url.href} type="video/mp4" /> - Not supported. + Not supported. </video> + {!this.audiopath || this.audiopath === field.url.href ? (null) : + <audio ref={this.setAudioRef} className={`audiobox-control${this.active() ? "-interactive" : ""}`}> + <source src={this.audiopath} type="audio/mpeg" /> + Not supported. + </audio>} </div> </div>; } @@ -461,7 +478,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } } else if (seekTimeInSeconds <= this.player.duration) { this.player.currentTime = seekTimeInSeconds; + this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds); this.player.play(); + this._audioPlayer?.play(); runInAction(() => this._playing = true); if (endTime !== this.duration) { this._playRegionTimer = setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration @@ -473,10 +492,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } playLink = (doc: Doc) => { - const startTime = this._stackedTimeline.current?.anchorStart(doc) || 0; + const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0) - .25); const endTime = this._stackedTimeline.current?.anchorEnd(doc); if (startTime !== undefined) { - if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this.Seek(startTime); } } @@ -491,6 +510,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}> <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} + mediaPath={this.audiopath} renderDepth={this.props.renderDepth + 1} startTag={"_timecodeToShow" /* videoStart */} endTag={"_timecodeToHide" /* videoEnd */} @@ -529,7 +549,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; scaling = () => this.props.scaling?.() || 1; panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; - panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / Doc.NativeAspect(this.rootDoc) : this.props.PanelHeight() * this.heightPercent / 100; + panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100; screenToLocalTransform = () => { const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); @@ -544,7 +564,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, borderRadius - }} > + }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}> <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > <div style={{ position: "absolute", transition: this.transition, width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index cdff4aa74..ca82c049c 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -18,26 +18,6 @@ border-radius: 3px; pointer-events: all; } - .webBox-tagList { - display: flex; - flex-direction: row; - overflow: auto; - flex-flow: row; - flex-wrap: wrap; - .webBox-filterTag, .webBox-filterTag-active { - font-weight: bold; - padding-left: 6; - padding-right: 6; - box-shadow: black 1px 1px 4px; - border-radius: 5; - margin: 2; - height: 20; - background-color: lightgrey; - } - .webBox-filterTag-active { - background-color: white; - } - } .pdfViewerDash-dragAnnotationBox { position: absolute; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index ed412ad99..3337865a5 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -8,19 +8,17 @@ import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; -import { listSpec, makeInterface } from "../../../fields/Schema"; +import { makeInterface, listSpec } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, getWordAtPoint, OmitKeys, returnOne, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { emptyFunction, getWordAtPoint, OmitKeys, returnOne, smoothScroll, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SnappingManager } from "../../util/SnappingManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CollectionStackingView } from "../collections/CollectionStackingView"; -import { CollectionViewType } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; @@ -29,13 +27,12 @@ import { LightboxView } from "../LightboxView"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { AnchorMenu } from "../pdf/AnchorMenu"; import { Annotation } from "../pdf/Annotation"; -import { SearchBox } from "../search/SearchBox"; -import { StyleProp } from "../StyleProvider"; +import { SidebarAnnos } from "../SidebarAnnos"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; import React = require("react"); +const _global = (window /* browser */ || global /* node */) as any; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -44,34 +41,90 @@ const WebDocument = makeInterface(documentSchema); @observer export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _keyInput = React.createRef<HTMLInputElement>(); - @observable _scrollTimer: any; - @observable private _overlayAnnoInfo: Opt<Doc>; private _initialScroll: Opt<number>; - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + private _sidebarRef = React.createRef<SidebarAnnos>(); + @observable private _scrollTimer: any; + @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; @observable private _isAnnotating = false; + @observable private _iframeClick: HTMLIFrameElement | undefined = undefined; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable private _scrollHeight = 1500; @computed get scrollHeight() { return this._scrollHeight; } + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } + @computed get webField() { return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url; } constructor(props: any) { super(props); - if (this.dataDoc[this.fieldKey] instanceof WebField) { + if (this.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); } if (this.layoutDoc[this.fieldKey + "-contentWidth"] === undefined) { this.layoutDoc[this.fieldKey + "-contentWidth"] = Doc.NativeWidth(this.layoutDoc); } - this._annotationKey = "annotations-" + this.urlHash(this._url); + } + + async componentDidMount() { + this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + + runInAction(() => { + this._url = this.webField?.toString() || ""; + this._annotationKey = "annotations-" + this.urlHash(this._url); + }); + + this._disposers.selection = reaction(() => this.props.isSelected(), + selected => !selected && setTimeout(() => { + Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.clear(); + })); + + if (this.webField?.href.indexOf("youtube") !== -1) { + const youtubeaspect = 400 / 315; + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); + if (this.webField) { + if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { + 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 (this.webField && !this.dataDoc.text) { + const result = await WebRequest.get(Utils.CorsProxy(this.webField.href)); + if (result) { + this.dataDoc.text = htmlToText.fromString(result.content); + } + } + + var quickScroll = true; + this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), + (scrollTop) => { + if (quickScroll) this._initialScroll = scrollTop; + else { + const viewTrans = StrCast(this.Document._viewTransition); + const durationMiliStr = viewTrans.match(/([0-9]*)ms/); + const durationSecStr = viewTrans.match(/([0-9.]*)s/); + const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; + this.goTo(scrollTop, duration); + } + }, + { fireImmediately: true } + ); + quickScroll = false; + } + componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); + this._iframe?.removeEventListener('wheel', this.iframeWheel, true); } @action @@ -95,11 +148,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum //this._selectionText = selRange.cloneContents().textContent || ""; // clear selection - if (sel.empty) { // Chrome - sel.empty(); - } else if (sel.removeAllRanges) { // Firefox - sel.removeAllRanges(); + if (sel.empty) sel.empty();// Chrome + else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox + } + + menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected + + scrollFocus = (doc: Doc, smooth: boolean) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; + if (doc !== this.rootDoc && this._outerRef.current) { + const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); + if (scrollTo !== undefined) { + const focusSpeed = smooth ? 500 : 0; + this._initialScroll !== undefined && (this._initialScroll = scrollTo); + this.goTo(scrollTo, focusSpeed); + return focusSpeed; + } } + this._initialScroll = NumCast(doc.y); + return 0; + } + + getAnchor = () => { + const anchor = Docs.Create.TextanchorDocument({ + title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), + annotationOn: this.rootDoc, + y: NumCast(this.layoutDoc._scrollTop), + }); + this.addDocument(anchor); + return anchor; } @action @@ -127,6 +204,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. } else { + this._iframeClick = this._iframe ?? undefined; this._isAnnotating = true; this.props.select(false); e.stopPropagation(); @@ -134,6 +212,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } + isFirefox = () => { + return "InstallTrigger" in window; // navigator.userAgent.indexOf("Chrome") !== -1; + } + iframeClick = () => this._iframeClick; + iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; + @action iframeLoaded = (e: any) => { const iframe = this._iframe; @@ -151,8 +235,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum 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.submitURL(href.replace(Utils.prepend(""), Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.origin)); + if (href && this.webField?.origin) { + this.submitURL(href.replace(Utils.prepend(""), this.webField?.origin)); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); this._outerRef.current.scrollLeft = 0; @@ -165,6 +249,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } @action + iframeWheel = (e: any) => { + if (!this._scrollTimer) { + this._scrollTimer = setTimeout(action(() => this._scrollTimer = undefined), 250); // this turns events off on the iframe which allows scrolling to change direction smoothly + } + } + + @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight()); timeout = scrollTop > iframeHeight ? 0 : timeout; @@ -177,92 +268,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } }), timeout); } - @action - iframeWheel = (e: any) => { - if (!this._scrollTimer) { - this._scrollTimer = setTimeout(action(() => this._scrollTimer = undefined), 250); // this turns events off on the iframe which allows scrolling to change direction smoothly - } - } - onWheel = (e: any) => { - e.stopPropagation(); - e.preventDefault(); - } - onScroll = (e: any) => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0); - scrollFocus = (doc: Doc, smooth: boolean) => { - if (doc !== this.rootDoc && this._outerRef.current) { - const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); - if (scrollTo !== undefined) { - const focusSpeed = smooth ? 500 : 0; - this._initialScroll !== undefined && (this._initialScroll = scrollTo); - this.goTo(scrollTo, focusSpeed); - return focusSpeed; - } - } - this._initialScroll = NumCast(doc.y); - return 0; - } - - getAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ - title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), - useLinkSmallAnchor: true, - hideLinkButton: true, - annotationOn: this.rootDoc, - y: NumCast(this.layoutDoc._scrollTop), - }); - this.addDocument(anchor); - return anchor; - } - - async componentDidMount() { - this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - - const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); - runInAction(() => this._url = urlField?.url.toString() || ""); - - this._disposers.selection = reaction(() => this.props.isSelected(), - selected => !selected && setTimeout(() => { - Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.clear(); - })); - - const field = Cast(this.rootDoc[this.props.fieldKey], WebField); - if (field?.url.href.indexOf("youtube") !== -1) { - const youtubeaspect = 400 / 315; - 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) 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 && !this.dataDoc.text) { - const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); - if (result) { - this.dataDoc.text = htmlToText.fromString(result.content); - } - } - - var quickScroll = true; - this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), - (scrollTop) => { - if (quickScroll) { - this._initialScroll = scrollTop; - } - else { - const viewTrans = StrCast(this.Document._viewTransition); - const durationMiliStr = viewTrans.match(/([0-9]*)ms/); - const durationSecStr = viewTrans.match(/([0-9.]*)s/); - const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; - this.goTo(scrollTop, duration); - } - }, - { fireImmediately: true } - ); - quickScroll = false; - } goTo = (scrollTop: number, duration: number) => { if (this._outerRef.current) { @@ -277,15 +282,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } - componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); - this._iframe?.removeEventListener('wheel', this.iframeWheel, true); - } - @action forward = () => { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (future.length) { history.push(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!)); @@ -297,8 +297,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @action back = () => { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (history.length) { if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); else future.push(this._url); @@ -309,17 +309,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return false; } - urlHash(s: string) { + urlHash = (s: string) => { return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); } @action - submitURL = (newUrl: string) => { + submitURL = (newUrl?: string) => { + if (!newUrl) return; if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl; try { - const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); - const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); - const url = Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.toString(); + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string")); + const url = this.webField?.toString(); if (url) { if (history === undefined) { this.dataDoc[this.fieldKey + "-history"] = new List<string>([url]); @@ -338,7 +339,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return true; } - menuControls = () => this.urlEditor; onWebUrlDrop = (e: React.DragEvent) => { const { dataTransfer } = e; const html = dataTransfer.getData("text/html"); @@ -373,12 +373,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> GO </button> - <button className="submitUrl" onClick={this.back}> - <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> - </button> - <button className="submitUrl" onClick={this.forward}> - <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> - </button> + <button className="submitUrl" onClick={this.back}> <FontAwesomeIcon icon="caret-left" size="lg" /> </button> + <button className="submitUrl" onClick={this.forward}> <FontAwesomeIcon icon="caret-right" size="lg" /> </button> </div> </div> ); @@ -393,6 +389,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } + @action + onMarqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.active(true)) { + this._marqueeing = [e.clientX, e.clientY]; + this.props.select(false); + } + } + @action finishMarquee = (x?: number, y?: number) => { + this._marqueeing = undefined; + this._isAnnotating = false; + this._iframeClick = undefined; + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false); + } + @computed get urlContent() { const field = this.dataDoc[this.props.fieldKey]; @@ -401,7 +411,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { 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"} style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded} @@ -416,101 +425,27 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return view; } - anchorMenuClick = (anchor: Doc) => { - this.Document._showSidebar = true; - const startup = StrListCast(this.rootDoc.docFilters).map(filter => filter.split(":")[0]).join(" "); - const target = Docs.Create.TextDocument(startup, { - title: "anno", - annotationOn: this.rootDoc, _width: 200, _height: 50, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily) - }); - FormattedTextBox.SelectOnLoad = target[Id]; - FormattedTextBox.DontSelectInitialText = true; - this.allTags.map(tag => target[tag] = tag); - DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup", "annotation"); - this.sidebarAddDocument(target); - } - toggleSidebar = () => { - if (this.layoutDoc.nativeWidth === this.layoutDoc[this.fieldKey + "-nativeWidth"]) { - this.layoutDoc.nativeWidth = 250 + NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } else { - this.layoutDoc.nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); - } - this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])); - } - sidebarKey = () => this.fieldKey + "-sidebar"; - sidebarFiltersHeight = () => 50; - sidebarTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.dataDoc), 0).scale(this.props.scaling?.() || 1); + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + return this.addDocument(doc, sidebarKey); + } + toggleSidebar = action(() => { + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth; + const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); + this.layoutDoc.nativeWidth = nativeWidth * ratio; + this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; + this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + }); sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); - sidebarHeight = () => this.props.PanelHeight() - this.sidebarFiltersHeight() - 20; - sidebarAddDocument = (doc: Doc | Doc[]) => this.addDocument(doc, this.sidebarKey()); - sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.sidebarKey()); - sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.sidebarKey()); - sidebarDocFilters = () => [...StrListCast(this.layoutDoc._docFilters), ...StrListCast(this.layoutDoc[this.sidebarKey() + "-docFilters"])]; - @computed get allTags() { - const keys = new Set<string>(); - DocListCast(this.rootDoc[this.sidebarKey()]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys.keys()).filter(key => key[0]).filter(key => !key.startsWith("_") && (key[0] === "#" || key[0] === key[0].toUpperCase())).sort(); - } - renderTag = (tag: string) => { - const active = StrListCast(this.rootDoc[this.sidebarKey() + "-docFilters"]).includes(`${tag}:${tag}:check`); - return <div className={`webBox-filterTag${active ? "-active" : ""}`} - onClick={e => Doc.setDocFilter(this.rootDoc, tag, tag, "check", true, this.sidebarKey())}> - {tag} - </div>; - } - @computed get sidebarOverlay() { - return !this.layoutDoc._showSidebar ? (null) : - <div style={{ - position: "absolute", pointerEvents: this.active() ? "all" : undefined, top: 0, right: 0, - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor), - width: `${this.sidebarWidth()}px`, - height: "100%" - }}> - <div style={{ width: "100%", height: this.sidebarHeight(), position: "relative" }}> - <CollectionStackingView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - NativeWidth={returnZero} - NativeHeight={returnZero} - PanelHeight={this.sidebarHeight} - PanelWidth={this.sidebarWidth} - xMargin={0} - yMargin={0} - docFilters={this.sidebarDocFilters} - chromeStatus={"enabled"} - scaleField={this.sidebarKey() + "-scale"} - isAnnotationOverlay={false} - select={emptyFunction} - active={this.annotationsActive} - scaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - childHideDecorationTitle={returnTrue} - removeDocument={this.sidebarRemDocument} - moveDocument={this.sidebarMoveDocument} - addDocument={this.sidebarAddDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarTransform} - renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} - fieldKey={this.sidebarKey()} - pointerEvents={"all"} - /> - </div> - <div className="webBox-tagList" style={{ height: this.sidebarFiltersHeight(), width: this.sidebarWidth() }}> - {this.allTags.map(tag => this.renderTag(tag))} - </div> - </div>; - } - @computed - get content() { - return <div className={"webBox-cont" + (this.active() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} + @computed get content() { + return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.active() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} style={{ width: NumCast(this.layoutDoc[this.fieldKey + "-contentWidth"]) || `${100 / (this.props.scaling?.() || 1)}%`, }}> {this.urlContent} </div>; } - showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get annotationLayer() { TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> @@ -520,20 +455,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum </div>; } - @action - onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.active(true)) { - this._marqueeing = [e.clientX, e.clientY]; - this.props.select(false); - } - } + showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; - @action finishMarquee = (x?: number, y?: number) => { - this._marqueeing = undefined; - this._isAnnotating = false; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false); - } - panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); @@ -541,7 +464,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false; const scale = this.props.scaling?.() || 1; return ( - <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.active() || SnappingManager.GetIsDragging() ? undefined : "none" }} > + <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.annotationsActive() ? "all" : this.active() || SnappingManager.GetIsDragging() ? undefined : "none" }} > <div className={`webBox-container`} style={{ pointerEvents: inactiveLayer ? "none" : undefined }} onContextMenu={this.specificContextMenu}> @@ -553,8 +476,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum transform: `scale(${scale})`, pointerEvents: inactiveLayer ? "none" : undefined }} - onWheel={this.onWheel} - onScroll={this.onScroll} + onWheel={e => { e.stopPropagation(); e.preventDefault(); }} // block wheel events from propagating since they're handled by the iframe + onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown} > <div className={"webBox-innerContent"} style={{ @@ -563,28 +486,34 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum }}> {this.content} <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - renderDepth={this.props.renderDepth + 1} - CollectionView={undefined} - fieldKey={this.annotationKey} isAnnotationOverlay={true} - scaling={returnOne} - pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} + fieldKey={this.annotationKey} + setPreviewCursor={this.setPreviewCursor} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.scrollXf} - setPreviewCursor={this.setPreviewCursor} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} + dropAction={"alias"} select={emptyFunction} active={this.active} - whenActiveChanged={this.whenActiveChanged} /> + ContentScaling={returnOne} + bringToFront={emptyFunction} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.sidebarAddDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.scrollXf} + renderDepth={this.props.renderDepth + 1} + scaling={returnOne} + childPointerEvents={true} + pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} /> {this.annotationLayer} </div> </div> {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : <MarqueeAnnotator rootDoc={this.rootDoc} - anchorMenuClick={this.anchorMenuClick} + iframe={this.isFirefox() ? this.iframeClick : undefined} + iframeScaling={this.isFirefox() ? this.iframeScaling : undefined} + anchorMenuClick={this._sidebarRef.current?.anchorMenuClick} scrollTop={0} down={this._marqueeing} scaling={returnOne} addDocument={this.addDocument} @@ -594,11 +523,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div > - <button className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" style={{ right: this.sidebarWidth() + 7 }} + <button className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" + style={{ right: this.sidebarWidth() + 7, display: !this.active() ? "none" : undefined }} onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} > <FontAwesomeIcon style={{ color: "white" }} icon={"chevron-left"} size="sm" /> </button> - {this.sidebarOverlay} + <SidebarAnnos ref={this._sidebarRef} + {...this.props} + fieldKey={this.annotationKey} + annotationsActive={this.annotationsActive} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + active={this.active} + /> </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d64db1ee1..abfc63b40 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -61,7 +61,6 @@ import { RichTextRules } from "./RichTextRules"; import { schema } from "./schema_rts"; import { SummaryView } from "./SummaryView"; import applyDevTools = require("prosemirror-dev-tools"); - import React = require("react"); const translateGoogleApi = require("translate-google-api"); @@ -113,7 +112,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _downEvent: any; private _downX = 0; private _downY = 0; - private _break = false; + private _break = true; public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; } public get SidebarKey() { return this.fieldKey + "-sidebar"; } @@ -125,8 +124,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); } @computed get sidebarHeight() { return NumCast(this.rootDoc[this.SidebarKey + "-height"]); } @computed get titleHeight() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } - @computed get _recording() { return this.dataDoc?.audioState === "recording"; } - set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } + @computed get _recording() { return this.dataDoc?.mediaState === "recording"; } + set _recording(value) { + !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? "recording" : undefined); + } @computed get config() { this._keymap = buildKeymap(schema, this.props); this._rules = new RichTextRules(this.props.Document, this); @@ -250,6 +251,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { + const accumTags = [] as string[]; + state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => { + if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith("#")) { + accumTags.push(node.attrs.fieldKey); + } + }); + const curTags = Object.keys(this.dataDoc).filter(key => key.startsWith("#")); + const added = accumTags.filter(tag => !curTags.includes(tag)); + const removed = curTags.filter(tag => !accumTags.includes(tag)); + removed.forEach(r => this.dataDoc[r] = undefined); + added.forEach(a => this.dataDoc[a] = a); + let unchanged = true; if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) { this._applyingChange = this.fieldKey; @@ -291,11 +304,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp insertTime = () => { let linkTime; let linkAnchor; + let link; DocListCast(this.dataDoc.links).forEach((l, i) => { const anchor = (l.anchor1 as Doc).annotationOn ? l.anchor1 as Doc : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined; - if (anchor && (anchor.annotationOn as Doc).audioState === "recording") { + if (anchor && (anchor.annotationOn as Doc).mediaState === "recording") { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; + link = l; } }); if (this._editorView && linkTime) { @@ -622,6 +637,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._downX = this._downY = Number.NaN; } + breakupDictation = () => { + if (this._editorView && this._recording) { + this.stopDictation(true); + this._break = true; + const state = this._editorView.state; + const to = state.selection.to; + const updated = TextSelection.create(state.doc, to, to); + this._editorView.dispatch(state.tr.setSelection(updated).insertText("\n", to)); + if (this._recording) { + this.recordDictation(); + } + } + } recordDictation = () => { DictationManager.Controls.listen({ interimHandler: this.setDictationContent, @@ -635,28 +663,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort); setDictationContent = (value: string) => { - if (this._editorView) { - const state = this._editorView.state; - const now = Date.now(); - let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) }); - if (!this._break && state.selection.to !== state.selection.from) { - for (let i = state.selection.from; i <= state.selection.to; i++) { - const pos = state.doc.resolve(i); - const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark); - if (um) { - mark = um; - break; - } - } + if (this._editorView && this._recordingStart) { + if (this._break) { + const textanchor = Docs.Create.TextanchorDocument({ title: "dictation anchor" }); + this.addDocument(textanchor); + const link = DocUtils.MakeLinkToActiveAudio(textanchor, false).lastElement(); + link && (Doc.GetProto(link).isDictation = true); + if (!link) return; + const audioanchor = Cast(link.anchor2, Doc, null); + if (!audioanchor) return; + audioanchor.backgroundColor = "tan"; + const audiotag = this._editorView.state.schema.nodes.audiotag.create({ + timeCode: NumCast(audioanchor._timecodeToShow), + audioId: audioanchor[Id], + textId: textanchor[Id] + }); + Doc.GetProto(textanchor).title = "dictation:" + audiotag.attrs.timeCode; + const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); + const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); } - const from = state.selection.from; + const from = this._editorView.state.selection.from; this._break = false; - if (this.props.Document.recordingStart) { - const recordingStart = DateCast(this.props.Document.recordingStart)?.date.getTime(); - value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value; - } - const tr = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, from + value.length + 1))); + const tr = this._editorView.state.tr.insertText(value); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView()); } } @@ -667,7 +697,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); if (sel.from !== sel.to) { - const anchor = anchorDoc ?? Docs.Create.TextanchorDocument(); + const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to) }); const href = targetHref ?? Utils.prepend("/doc/" + anchor[Id]); if (anchor !== anchorDoc) this.addDocument(anchor); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { @@ -681,7 +711,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; - Doc.GetProto(anchor).title = this._editorView?.state.doc.textBetween(sel.from, sel.to); return anchor; } return anchorDoc ?? this.rootDoc; @@ -689,14 +718,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return anchorDoc ?? this.rootDoc; } - scrollFocus = (doc: Doc, smooth: boolean) => { - const anchorId = doc[Id]; + scrollFocus = (textAnchor: Doc, smooth: boolean) => { + const textAnchorId = textAnchor[Id]; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; let hadStart = start !== 0; frag.forEach((node, index) => { const examinedNode = findAnchorNode(node, editor); - if (examinedNode?.node.textContent) { + if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); !hadStart && (start = index + examinedNode.start); hadStart = true; @@ -705,31 +734,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return { frag: Fragment.fromArray(nodes), start }; }; const findAnchorNode = (node: Node, editor: EditorView) => { + if (node.type === this._editorView?.state.schema.nodes.audiotag) { + if (node.attrs.textId === textAnchorId) { + return { node, start: 0 }; + } + return undefined; + } if (!node.isText) { const content = findAnchorFrag(node.content, editor); 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.allAnchors.find((item: { href: string }) => anchorId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined; + return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => textAnchorId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined; }; let start = 0; - if (this._editorView && anchorId) { + if (this._editorView && textAnchorId) { const editor = this._editorView; const ret = findAnchorFrag(editor.state.doc.content, editor); - if (ret.frag.size > 2 && ret.start >= 0) { + const content = (ret.frag as any)?.content; + if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { smooth && (this._focusSpeed = 500); let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected } editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); - const escAnchorId = anchorId[0] >= '0' && anchorId[0] <= '9' ? `\\3${anchorId[0]} ${anchorId.substr(1)}` : anchorId; - addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" }); + const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; + addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow", transform: "scale(3)", "transform-origin": "left bottom" }); setTimeout(() => this._focusSpeed = undefined, this._focusSpeed); - setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 1500)); + setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); } } @@ -748,9 +784,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this.props.contentsActive?.(this.active); this._cachedLinks = DocListCast(this.Document.links); + this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); this._disposers.autoHeight = reaction(() => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight()); - this._disposers.autoHeight = reaction(() => ({ scrollHeight: this.scrollHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight }) => width && this.autoHeight && this.resetNativeHeight(scrollHeight) + this._disposers.scrollHeight = reaction(() => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }), + ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight) ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight }), @@ -817,7 +854,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.selected = reaction(() => this.props.isSelected(), action((selected) => { - this._recording = false; if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } @@ -826,14 +862,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (!this.props.dontRegisterView) { this._disposers.record = reaction(() => this._recording, () => { + this.stopDictation(true); if (this._recording) { - setTimeout(action(() => { - this.stopDictation(true); - setTimeout(() => this.recordDictation(), 500); - }), 500); - } else setTimeout(() => this.stopDictation(true), 0); - } + this.recordDictation(); + } + }, ); + if (this._recording) setTimeout(() => this.recordDictation()); } var quickScroll: string | undefined = ""; this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop), @@ -1034,11 +1069,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView = new EditorView(this.ProseRef, { state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: (editorView) => { - const docPos = editorView.coordsAtPos(editorView.state.selection.from); + const docPos = editorView.coordsAtPos(editorView.state.selection.to); const viewRect = self._ref.current!.getBoundingClientRect(); const scrollRef = self._scrollRef.current; - if ((docPos.top < viewRect.top || docPos.top > viewRect.bottom) && scrollRef) { - const scrollPos = scrollRef.scrollTop + (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale; + if ((docPos.bottom < viewRect.top || docPos.bottom > viewRect.bottom) && scrollRef) { + const scrollPos = scrollRef.scrollTop + (docPos.bottom - viewRect.top) * self.props.ScreenToLocalTransform().Scale; if (this._focusSpeed !== undefined) { scrollPos && smoothScroll(this._focusSpeed, scrollRef, scrollPos); } else { @@ -1107,26 +1142,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((e.target as any).tagName === "AUDIOTAG") { e.preventDefault(); e.stopPropagation(); - const time = (e.target as any)?.dataset?.timecode || 0; - const audioid = (e.target as any)?.dataset?.audioid || 0; - DocServer.GetRefField(audioid).then(anchor => { + DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => { if (anchor instanceof Doc) { + const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; - audiodoc._triggerAudio = Number(time); - !DocumentManager.Instance.getDocumentView(audiodoc) && this.props.addDocTab(audiodoc, "add:bottom"); + const func = () => { + const docView = DocumentManager.Instance.getDocumentView(audiodoc); + if (!docView) { + this.props.addDocTab(audiodoc, "add:bottom"); + setTimeout(func); + } + else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, "number", null)); // bcz: would be nice to find the next audio tag in the doc and play until that + }; + func(); } }); } if (this._recording && !e.ctrlKey && e.button === 0) { - this.stopDictation(true); - this._break = true; - const state = this._editorView!.state; - const to = state.selection.to; - const updated = TextSelection.create(state.doc, to, to); - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(updated).insertText("\n", to)); + this.breakupDictation(); e.preventDefault(); e.stopPropagation(); - if (this._recording) setTimeout(() => this.recordDictation(), 500); } this._downX = e.clientX; this._downY = e.clientY; @@ -1239,10 +1274,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp e.stopPropagation(); return; } - this.props.isSelected(true) && ((e.nativeEvent as any).formattedHandled = true); - if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events - // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks + (e.nativeEvent as any).formattedHandled = true; + if (this.ProseRef?.children[0] !== e.nativeEvent.target) e.stopPropagation(); // if you double click on text, then it will be selected instead of sending a double click to DocumentView & opening a lightbox. Also,if a text box has isLinkButton, this will prevent link following if you've selected the document to edit it. + // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks (see above comment) this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey); } this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; @@ -1383,14 +1418,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } tryUpdateScrollHeight() { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - const proseHeight = this.ProseRef?.scrollHeight || 0; - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); - if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; - if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { - setScrollHeight(); - } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... - } + setTimeout(() => { // bcz: don't know why this is needed, but without it, the size of the textbox is too big as it includes the size of the title header. after the timeout, the size seems to get computed correctly. + const proseHeight = this.ProseRef?.scrollHeight || 0; + const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; + if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { + setScrollHeight(); + } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... + } + }); } } fitToBox = () => this.props.Document._fitToBox; @@ -1427,7 +1464,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp PanelWidth={this.sidebarWidth} xMargin={0} yMargin={0} - chromeStatus={"enabled"} scaleField={this.SidebarKey + "-scale"} isAnnotationOverlay={false} select={emptyFunction} diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 4b9b78211..3fd7d61fa 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -90,7 +90,7 @@ export class RichTextRules { textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9px", title: "inline comment" }); + const textDocInline = Docs.Create.TextDocument("", { _layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9px", title: "inline comment" }); textDocInline.title = inlineFieldKey; // give the annotation its own title textDocInline["title-custom"] = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index df93fc117..2fe0a67cb 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -28,14 +28,17 @@ export const nodes: { [index: string]: NodeSpec } = { group: "block", attrs: { timeCode: { default: 0 }, - audioId: { default: "" } + audioId: { default: "" }, + textId: { default: "" } }, toDOM(node) { return ['audiotag', { + class: node.attrs.textId, // style: see FormattedTextBox.scss "data-timecode": node.attrs.timeCode, "data-audioid": node.attrs.audioId, + "data-textid": node.attrs.textId, }, formatAudioTime(node.attrs.timeCode.toString()) ]; @@ -45,7 +48,8 @@ export const nodes: { [index: string]: NodeSpec } = { tag: "audiotag", getAttrs(dom: any) { return { timeCode: dom.getAttribute("data-timecode"), - audioId: dom.getAttribute("data-audioid") + audioId: dom.getAttribute("data-audioid"), + textId: dom.getAttribute("data-textid") }; } }, diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index 8b242854d..e98f071c3 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -1,4 +1,4 @@ -.pdfAnnotation { +.htmlAnnotation { pointer-events: all; user-select: none; position: absolute; diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 1e574b9c0..07b734405 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -89,7 +89,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { } render() { - return (<div className="pdfAnnotation" onPointerEnter={() => this.props.showInfo(this.props.anno)} onPointerLeave={() => this.props.showInfo(undefined)} onPointerDown={this.onPointerDown} ref={this._mainCont} + return (<div className="htmlAnnotation" onPointerEnter={() => this.props.showInfo(this.props.anno)} onPointerLeave={() => this.props.showInfo(undefined)} onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ left: NumCast(this.props.document.x), top: NumCast(this.props.document.y), diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 68b1452f8..84006f722 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -55,7 +55,7 @@ interface IViewerProps extends FieldViewProps { setPdfViewer: (view: PDFViewer) => void; ContentScaling?: () => number; sidebarWidth: () => number; - anchorMenuClick: (anchor: Doc) => void; + anchorMenuClick?: (anchor: Doc) => void; } /** @@ -391,7 +391,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu AnchorMenu.Instance.Status = "marquee"; Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.clear(); - this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); + this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "htmlAnnotation", { "pointer-events": "none" }); document.addEventListener("pointerup", this.onSelectEnd); document.addEventListener("pointermove", this.onSelectMove); } @@ -532,13 +532,13 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu ContentScaling={this.contentZoom} bringToFront={emptyFunction} whenActiveChanged={this.whenActiveChanged} - childPointerEvents={true} removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument} CollectionView={undefined} ScreenToLocalTransform={this.overlayTransform} - renderDepth={this.props.renderDepth + 1} /> + renderDepth={this.props.renderDepth + 1} + childPointerEvents={true} /> </div>; } @computed get pdfViewerDiv() { diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 1c9ab0e7e..31043f5be 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -12,7 +12,6 @@ import { UndoManager } from "../client/util/UndoManager"; import { intersectRect, Utils } from "../Utils"; import { DateField } from "./DateField"; import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols"; -import { InkTool } from "./InkField"; import { List } from "./List"; import { ObjectField } from "./ObjectField"; import { PrefetchProxy, ProxyField } from "./Proxy"; @@ -85,6 +84,7 @@ export const DataSym = Symbol("Data"); export const LayoutSym = Symbol("Layout"); export const FieldsSym = Symbol("Fields"); export const AclSym = Symbol("Acl"); +export const DirectLinksSym = Symbol("DirectLinks"); export const AclUnset = Symbol("AclUnset"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); @@ -92,6 +92,7 @@ export const AclAddonly = Symbol("AclAddonly"); export const AclEdit = Symbol("AclEdit"); export const AclAdmin = Symbol("AclAdmin"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); +export const Initializing = Symbol("Initializing"); export const ForceServerWrite = Symbol("ForceServerWrite"); export const CachedUpdates = Symbol("Cached updates"); @@ -184,9 +185,11 @@ export class Doc extends RefField { @observable private ___fields: any = {}; @observable private ___fieldKeys: any = {}; @observable public [AclSym]: { [key: string]: symbol }; + @observable public [DirectLinksSym]: Set<Doc> = new Set(); private [UpdatingFromServer]: boolean = false; private [ForceServerWrite]: boolean = false; + public [Initializing]: boolean = false; private [Update] = (diff: any) => { (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff); @@ -369,7 +372,8 @@ export namespace Doc { * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key * to a potentially undefined field, where each entry in this mapping is optional. */ - export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false) { + export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) { + isInitializing && (doc[Initializing] = true); for (const key in fields) { if (fields.hasOwnProperty(key)) { const value = fields[key]; @@ -378,6 +382,7 @@ export namespace Doc { } } } + isInitializing && (doc[Initializing] = false); return doc; } @@ -777,10 +782,12 @@ export namespace Doc { export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> { if (doc) { const delegate = new Doc(id, true); + delegate[Initializing] = true; delegate.proto = doc; delegate.author = Doc.CurrentUserEmail; if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], "aliases", delegate); title && (delegate.title = title); + delegate[Initializing] = false; return delegate; } return undefined; @@ -1080,6 +1087,7 @@ export namespace Doc { if (!docFilters.length && modifiers === "match" && value === undefined) { container[filterField] = undefined; } else if (modifiers !== "remove") { + !append && (docFilters.length = 0); docFilters.push(key + ":" + value + ":" + modifiers); container[filterField] = new List<string>(docFilters); } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 9345ecde5..bd93bf5fb 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -150,7 +150,7 @@ export class ScriptField extends ObjectField { } public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Field }) { const compiled = CompileScript(script, { - params: { this: Doc.name, self: Doc.name, _last_: "any", ...params }, + params: { this: Doc?.name || "Doc", self: Doc?.name || "Doc", _last_: "any", ...params }, typecheck: false, editable: true, addReturn: addReturn, diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 0bf942474..275249840 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -45,8 +45,6 @@ export const documentSchema = createSchema({ _showAudio: "boolean", // whether to show the audio record icon on documents _freeformLOD: "boolean", // whether to enable LOD switching for CollectionFreeFormViews _pivotField: "string", // specifies which field key should be used as the timeline/pivot axis - _replacedChrome: "string", // what the default chrome is replaced with. Currently only supports the value of 'replaced' for PresBox's. - _chromeStatus: "string", // determines the state of the collection chrome. values allowed are 'replaced', 'enabled', 'disabled', 'collapsed' _columnsFill: "boolean", // whether documents in a stacking view column should be sized to fill the column _columnsSort: "string", // how a document should be sorted "ascending", "descending", undefined (none) _columnsHideIfEmpty: "boolean", // whether empty stacking view column headings should be hidden diff --git a/src/fields/util.ts b/src/fields/util.ts index 6038a0534..ea91cc057 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -24,7 +24,6 @@ export function TraceMobx() { tracing && trace(); } - export interface GetterResult { value: FieldResult; shouldReturn?: boolean; @@ -96,9 +95,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ + !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ redo: () => receiver[prop] = value, - undo: () => receiver[prop] = curValue + undo: () => { + // console.log("Undo: " + prop + " = " + curValue); // bcz: uncomment to log undo + receiver[prop] = curValue; + } }); return true; } @@ -162,7 +164,7 @@ export function GetEffectiveAcl(target: any, user?: string): symbol { } 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 === UpdatingFromServer || prop === Initializing || 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); } @@ -274,8 +276,6 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc 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 = getPropAcl(target, prop); @@ -285,10 +285,6 @@ export function setter(target: any, in_prop: string | symbol | number, value: an // 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("_")) { - // if (!prop.startsWith("_")) { - // console.log(prop + " is deprecated - switch to _" + prop); - // prop = "_" + prop; - // } if (!prop.startsWith("__")) prop = prop.substring(1); if (target.__LAYOUT__) { target.__LAYOUT__[prop] = value; @@ -380,6 +376,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any lastValue = ObjectField.MakeCopy(receiver[prop]); }, undo: action(() => { + // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo diff.items.forEach((item: any) => { const ind = receiver[prop].indexOf(item.value ? item.value() : item); ind !== -1 && receiver[prop].splice(ind, 1); @@ -397,6 +394,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any lastValue = ObjectField.MakeCopy(receiver[prop]); }), undo: () => { + // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log 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); @@ -410,6 +408,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any lastValue = ObjectField.MakeCopy(receiver[prop]); }, undo: () => { + // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); lastValue = ObjectField.MakeCopy(receiver[prop]); } diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index d9b38c014..ff6b2381c 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -16,6 +16,7 @@ import { resolvedServerUrl } from "./server_Initialization"; import { AcceptableMedia, Upload } from './SharedMediaTypes'; import request = require('request-promise'); const parse = require('pdf-parse'); +const ffmpeg = require("fluent-ffmpeg"); const requestImageSize = require("../client/util/request-image-size"); export enum SizeSuffix { @@ -62,7 +63,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}).`)); + console.log(green(`Processing upload of file (${name}) and format (${format}) with upload type (${type}) in category (${category}).`)); switch (category) { case "image": @@ -71,6 +72,14 @@ export namespace DashUploadUtils { return { source: file, result }; } case "video": + if (format.includes("x-matroska")) { + await new Promise(res => ffmpeg(file.path) + .videoCodec("copy") // this will copy the data instead of reencode it + .save(file.path.replace(".mkv", ".mp4")) + .on('end', res)); + file.path = file.path.replace(".mkv", ".mp4"); + format = ".mp4"; + } if (videoFormats.includes(format)) { return MoveParsedFile(file, Directory.videos); } diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index f1fe582e5..fdc65188f 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", ".quicktime"]; + export const videoFormats = [".mov", ".mp4", ".quicktime", ".mkv", ".x-matroska;codecs=avc1"]; export const applicationFormats = [".pdf"]; export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"]; } |