diff options
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 78 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 87 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 109 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.scss | 1 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 39 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 7 | ||||
-rw-r--r-- | src/client/views/nodes/KeyValuePair.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/LabelBox.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/LinkAnchorBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/PresBox.tsx | 30 | ||||
-rw-r--r-- | src/client/views/nodes/ScreenshotBox.tsx | 13 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 66 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 203 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 11 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts | 18 |
16 files changed, 540 insertions, 136 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 5d6e587d9..682aed8f5 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -10,9 +10,13 @@ import React = require("react"); import { Document } from "../../../fields/documentSchemas"; import { TraceMobx } from "../../../fields/util"; import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; +import { List } from "../../../fields/List"; +import { numberRange } from "../../../Utils"; +import { ComputedField } from "../../../fields/ScriptField"; +import { listSpec } from "../../../fields/Schema"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, highlight?: boolean, z: number, transition?: string } | undefined; + dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined; sizeProvider?: (doc: Doc, replica: string) => { width: number, height: number } | undefined; zIndex?: number; highlight?: boolean; @@ -31,10 +35,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const rnd = seed / 233280; return min + rnd * (max - min); } - get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive + get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; } get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); } get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); } + get Opacity() { return this.dataProvider ? this.dataProvider.opacity : Cast(this.layoutDoc.opacity, "number", null); } get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : (this.Document.zIndex || 0); } get Highlight() { return this.dataProvider?.highlight; } get width() { return this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.width : this.layoutDoc[WidthSym](); } @@ -62,6 +67,61 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } return undefined; } + + public static getValues(doc: Doc, time: number) { + const timecode = Math.round(time); + return ({ + x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number), + y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number), + opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number), + }); + } + + public static setValues(time: number, d: Doc, x?: number, y?: number, opacity?: number) { + const timecode = Math.round(time); + Cast(d["x-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = x as any as number; + Cast(d["y-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = y as any as number; + Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number; + Cast(d["y-indexed"], listSpec("number"), [])[timecode] = y as any as number; + Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number; + } + public static updateKeyframe(docs: Doc[], time: number) { + const timecode = Math.round(time); + docs.forEach(doc => { + const xindexed = Cast(doc['x-indexed'], listSpec("number"), null); + const yindexed = Cast(doc['y-indexed'], listSpec("number"), null); + const opacityindexed = Cast(doc['opacity-indexed'], listSpec("number"), null); + xindexed?.length <= timecode + 1 && xindexed.push(undefined as any as number); + yindexed?.length <= timecode + 1 && yindexed.push(undefined as any as number); + opacityindexed?.length <= timecode + 1 && opacityindexed.push(undefined as any as number); + doc.transition = "all 1s"; + }); + setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010); + } + + public static gotoKeyframe(docs: Doc[]) { + docs.forEach(doc => doc.transition = "all 1s"); + setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010); + } + + public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { + docs.forEach((doc, i) => { + const curTimecode = progressivize ? i : timecode; + const xlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); + const ylist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); + const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < i ? 0 : 1)); + xlist[Math.max(curTimecode - 1, 0)] = xlist[curTimecode] = NumCast(doc.x); + ylist[Math.max(curTimecode - 1, 0)] = ylist[curTimecode] = NumCast(doc.y); + doc["x-indexed"] = xlist; + doc["y-indexed"] = ylist; + doc["opacity-indexed"] = olist; + doc.activeFrame = ComputedField.MakeFunction("self.context ? (self.context.currentFrame||0) : 0"); + doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); + doc.y = ComputedField.MakeInterpolated("y", "activeFrame"); + doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame"); + }); + } + nudge = (x: number, y: number) => { this.props.Document.x = NumCast(this.props.Document.x) + x; this.props.Document.y = NumCast(this.props.Document.y) + y; @@ -72,14 +132,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF panelHeight = () => (this.sizeProvider?.height || this.props.PanelHeight?.()); getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y).scale(1 / this.contentScaling()); focusDoc = (doc: Doc) => this.props.focus(doc, false); + opacity = () => this.Opacity; NativeWidth = () => this.nativeWidth; NativeHeight = () => this.nativeHeight; render() { TraceMobx(); + const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); return <div className="collectionFreeFormDocumentView-container" style={{ boxShadow: - this.layoutDoc.opacity === 0 ? undefined : // if it's not visible, then no shadow + this.Opacity === 0 ? undefined : // if it's not visible, then no shadow this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big @@ -92,8 +154,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF height: this.height, zIndex: this.ZInd, display: this.ZInd === -99 ? "none" : undefined, - pointerEvents: this.props.Document.isBackground ? "none" : this.props.pointerEvents ? "all" : undefined + pointerEvents: this.props.Document.isBackground || this.Opacity === 0 ? "none" : this.props.pointerEvents ? "all" : undefined }} > + {Doc.UserDoc().renderStyle !== "comic" ? (null) : + <div style={{ width: "100%", height: "100%", position: "absolute" }}> + <svg style={{ transform: `scale(1,${this.props.PanelHeight() / this.props.PanelWidth()})`, transformOrigin: "top left", overflow: "visible" }} viewBox="0 0 12 14"> + <path d="M 7 0 C 9 -1 13 1 12 4 C 11 10 13 12 10 12 C 6 12 7 13 2 12 Q -1 11 0 8 C 1 4 0 4 0 2 C 0 0 1 0 1 0 C 3 0 3 1 7 0" + style={{ stroke: "black", fill: backgroundColor, strokeWidth: 0.2 }} /> + </svg> + </div>} {!this.props.fitToBox ? <DocumentView {...this.props} @@ -102,6 +171,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} backgroundColor={this.props.backgroundColor} + opacity={this.opacity} NativeHeight={this.NativeHeight} NativeWidth={this.NativeWidth} PanelWidth={this.panelWidth} diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss new file mode 100644 index 000000000..7849c9976 --- /dev/null +++ b/src/client/views/nodes/ComparisonBox.scss @@ -0,0 +1,87 @@ +.comparisonBox-interactive, .comparisonBox { + border-radius: inherit; + width: 100%; + height: 100%; + position: absolute; + z-index: 0; + pointer-events: none; + + .clip-div { + position: absolute; + top: 0; + left: 0; + height: 100%; + overflow: hidden; + + .beforeBox-cont { + height: 100%; + overflow: hidden; + } + } + + .slide-bar { + position: absolute; + height: 100%; + width: 3px; + display: inline-block; + background: white; + cursor: ew-resize; + .slide-handle { + position: absolute; + display: none; + height: 20px; + width: 30px; + bottom: 0px; + left: -10.5px; + .left-handle, .right-handle{ + width: 15px; + } + } + } + + .afterBox-cont { + position: absolute; + top: 0; + right: 0; + height: 100%; + width: 100%; + overflow: hidden; + } + + .clear-button { + position: absolute; + top: 3px; + opacity: 0.8; + pointer-events: all; + cursor: pointer; + } + + .clear-button.before { + left: 3px; + } + + .clear-button.after { + right: 3px; + } + + .placeholder { + width: 50%; + height: 50%; + margin-top: 25%; + margin-left: 25%; + + .upload-icon { + width: 100%; + height: 100%; + opacity: 0.5; + } + } +} +.comparisonBox-interactive { + pointer-events: unset; + .slide-bar { + .slide-handle { + display: flex; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx new file mode 100644 index 000000000..77e07ec0c --- /dev/null +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -0,0 +1,109 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction, Lambda, IReactionDisposer } from 'mobx'; +import { observer } from "mobx-react"; +import { Doc, Opt } from '../../../fields/Doc'; +import { documentSchema } from '../../../fields/documentSchemas'; +import { createSchema, makeInterface } from '../../../fields/Schema'; +import { NumCast, Cast, StrCast } from '../../../fields/Types'; +import { DragManager } from '../../util/DragManager'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ComparisonBox.scss"; +import React = require("react"); +import { ContentFittingDocumentView } from './ContentFittingDocumentView'; +import { undoBatch } from '../../util/UndoManager'; +import { setupMoveUpEvents, emptyFunction } from '../../../Utils'; +import { SnappingManager } from '../../util/SnappingManager'; +import { DocumentViewProps } from './DocumentView'; + +export const comparisonSchema = createSchema({}); + +type ComparisonDocument = makeInterface<[typeof comparisonSchema, typeof documentSchema]>; +const ComparisonDocument = makeInterface(comparisonSchema, documentSchema); + +@observer +export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, ComparisonDocument>(ComparisonDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } + protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; + private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; + + @observable _animating = ""; + + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { + this._disposers[disposerId]?.(); + if (ele) { + this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc); + } + } + + @undoBatch + private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + event.stopPropagation(); + const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; + if (droppedDocs?.length) { + this.dataDoc[fieldKey] = droppedDocs[0]; + } + } + + private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { + this._animating = "all 1s"; + this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth(); + setTimeout(action(() => this._animating = ""), 1000); + }), false); + } + + @action + private onPointerMove = ({ movementX }: PointerEvent) => { + const width = movementX * this.props.ScreenToLocalTransform().Scale + NumCast(this.layoutDoc._clipWidth) / 100 * this.props.PanelWidth(); + if (width && width > 5 && width < this.props.PanelWidth()) { + this.layoutDoc._clipWidth = width * 100 / this.props.PanelWidth(); + } + return false; + } + + @undoBatch + clearDoc = (e: React.MouseEvent, fieldKey: string) => { + e.stopPropagation; + delete this.dataDoc[fieldKey]; + } + + render() { + const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%"; + const childProps: DocumentViewProps = { ...this.props, pointerEvents: false, parentActive: this.props.active }; + const clearButton = (which: string) => { + return <div className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} onClick={e => this.clearDoc(e, `${which}Doc`)}> + <FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" /> + </div>; + }; + const displayDoc = (which: string) => { + const whichDoc = Cast(this.dataDoc[`${which}Doc`], Doc, null); + return whichDoc ? <> + <ContentFittingDocumentView {...childProps} Document={whichDoc} /> + {clearButton(which)} + </> : // placeholder image if doc is missing + <div className="placeholder"> + <FontAwesomeIcon className="upload-icon" icon={"cloud-upload-alt"} size="lg" /> + </div>; + }; + const displayBox = (which: string, index: number, cover: number) => { + return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} + onPointerDown={e => this.registerSliding(e, cover)} + ref={ele => this.createDropTarget(ele, `${which}Doc`, index)} > + {displayDoc(which)} + </div>; + }; + + return ( + <div className={`comparisonBox${this.active() || SnappingManager.GetIsDragging() ? "-interactive" : ""}`}> + {displayBox("after", 1, this.props.PanelWidth() - 5)} + <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}> + {displayBox("before", 0, 5)} + </div> + + <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)` }}> + <div className="slide-handle" /> + </div> + </div >); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index f4785bb0c..ef56e6fcd 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -30,6 +30,7 @@ import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { LinkAnchorBox } from "./LinkAnchorBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { ScreenshotBox } from "./ScreenshotBox"; +import { ComparisonBox } from "./ComparisonBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; @@ -194,7 +195,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, - RecommendationsBox, ScreenshotBox, HTMLtag + RecommendationsBox, ScreenshotBox, HTMLtag, ComparisonBox }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index dea09cb30..b7726f7ba 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -94,6 +94,7 @@ text-align: center; text-overflow: ellipsis; white-space: pre; + position: absolute; } .documentView-titleWrapper-hover { display:none; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 95af52f13..b5de0af12 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -12,7 +12,6 @@ import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils"; @@ -92,6 +91,7 @@ export interface DocumentViewProps { pinToPres: (document: Doc) => void; backgroundHalo?: () => boolean; backgroundColor?: (doc: Doc) => string | undefined; + opacity?: () => number | undefined; ChromeHeight?: () => number; dontRegisterView?: boolean; layoutKey?: string; @@ -683,12 +683,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true; } - @undoBatch - @action - toggleLockTransform = (): void => { - this.Document.lockedTransform = this.Document.lockedTransform ? undefined : true; - } - @action onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 @@ -751,7 +745,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; 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 !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - moreItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" }); moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); if (!ClientUtils.RELEASE) { @@ -765,10 +758,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); } moreItems.push({ - description: "Download document", icon: "download", event: async () => - console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { + description: "Download document", icon: "download", event: async () => { + const response = await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } - }))) + }); + console.log(response ? JSON.parse(response) : undefined); + } // const a = document.createElement("a"); // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); // a.href = url; @@ -1117,9 +1112,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }), 400); }); + renderLock() { + return (this.Document.isBackground !== undefined || this.isSelected(false)) && + ((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) && + this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? + <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> + <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> + </div> + : (null); + } + render() { if (!(this.props.Document instanceof Doc)) return (null); - const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); + const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); + const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null))); + const finalOpacity = this.props.opacity ? this.props.opacity() : opacity; const finalColor = this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor; const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); const borderRounding = this.layoutDoc.borderRounding; @@ -1156,7 +1163,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined, background: finalColor, - opacity: this.Document.opacity, + opacity: finalOpacity, fontFamily: StrCast(this.Document._fontFamily, "inherit"), fontSize: Cast(this.Document._fontSize, "number", null) }}> @@ -1165,11 +1172,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu <div className="documentView-contentBlocker" /> </> : this.innards} - {(this.Document.isBackground !== undefined || this.isSelected(false)) && this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? - <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> - <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> - </div> - : (null)} + {this.renderLock()} </div>; { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined; } } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 47e7607d6..6913dfbc7 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -19,7 +19,6 @@ import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_ser import { Docs } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; -import { SelectionManager } from '../../util/SelectionManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -267,7 +266,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) { requestImageSize(imgPath).then(action((inquiredSize: any) => { const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180; - const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize; + const rotatedNativeSize = { width: inquiredSize.width, height: inquiredSize.height }; + if (inquiredSize.orientation === 6 || rotation === 90 || rotation === 270) { + rotatedNativeSize.width = inquiredSize.height; + rotatedNativeSize.height = inquiredSize.width; + } const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 956d6556b..3cbe3e494 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -99,9 +99,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <div className="keyValuePair-td-key-container"> <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { - props.Document[props.fieldKey] = undefined; + delete props.Document[props.fieldKey]; } - else props.Document.proto![props.fieldKey] = undefined; + else delete props.Document.proto![props.fieldKey]; })}> X </button> diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 2d27ec441..ad9e49369 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,5 +1,3 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit } from '@fortawesome/free-regular-svg-icons'; import { action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -16,9 +14,6 @@ import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LabelBox.scss'; - -library.add(faEdit as any); - const LabelSchema = createSchema({}); type LabelDocument = makeInterface<[typeof LabelSchema, typeof documentSchema]>; diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 098aa58e9..83245a89c 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -72,7 +72,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { this._timeout = setTimeout(action(() => { - DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, "inTab")), false); + DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, e.altKey ? "inTab" : "onRight")), false); this._editing = false; }), 300 - (Date.now() - this._lastTap)); } diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 342a8a215..05306c29f 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -1,11 +1,11 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, DocCastAsync } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { returnFalse } from "../../../Utils"; +import { returnFalse, returnOne } from "../../../Utils"; import { documentSchema } from "../../../fields/documentSchemas"; import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; @@ -16,7 +16,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./PresBox.scss"; import { ViewBoxBaseComponent } from "../DocComponent"; import { makeInterface } from "../../../fields/Schema"; -import { List } from "../../../fields/List"; import { Docs } from "../../documents/Documents"; import { PrefetchProxy } from "../../../fields/Proxy"; import { ScriptField } from "../../../fields/ScriptField"; @@ -59,7 +58,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action next = () => { this.updateCurrentPresentation(); - if (this.childDocs[this.itemIndex + 1] !== undefined) { + const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); + const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); + const curFrame = NumCast(presTargetDoc.currentFrame); + if (lastFrame !== undefined && curFrame < lastFrame) { + presTargetDoc.currentFrame = curFrame + 1; + } + else if (this.childDocs[this.itemIndex + 1] !== undefined) { let nextSelected = this.itemIndex + 1; this.gotoDocument(nextSelected, this.itemIndex); @@ -188,11 +193,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //The function that is called when a document is clicked or reached through next or back. //it'll also execute the necessary actions if presentation is playing. - public gotoDocument = (index: number, fromDoc: number) => { + public gotoDocument = action((index: number, fromDoc: number) => { this.updateCurrentPresentation(); Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; + const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); + if (presTargetDoc.lastFrame !== undefined) { + presTargetDoc.currentFrame = 0; + } if (!this.layoutDoc.presStatus) { this.layoutDoc.presStatus = true; @@ -203,7 +212,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.hideIfNotPresented(index); this.showAfterPresented(index); } - } + }); //The function that starts or resets presentaton functionally, depending on status flag. startOrResetPres = () => { @@ -286,7 +295,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) render() { - this.rootDoc.presOrderedDocs = new List<Doc>(this.childDocs.map((child, i) => child)); + // console.log("render = " + this.layoutDoc.title + " " + this.layoutDoc.presStatus); + // const presOrderedDocs = DocListCast(this.rootDoc.presOrderedDocs); + // if (presOrderedDocs.length != this.childDocs.length || presOrderedDocs.some((pd, i) => pd !== this.childDocs[i])) { + // this.rootDoc.presOrderedDocs = new List<Doc>(this.childDocs.slice()); + // } + this.childDocs.slice(); // needed to insure that the childDocs are loaded for looking up fields const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; return <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined }} > <div className="presBox-buttons" style={{ display: this.rootDoc._chromeStatus === "disabled" ? "none" : undefined }}> @@ -316,6 +330,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> PanelWidth={this.props.PanelWidth} PanelHeight={this.panelHeight} moveDocument={returnFalse} + childOpacity={returnOne} childLayoutTemplate={this.childLayoutTemplate} filterAddDocument={this.addDocumentFilter} removeDocument={returnFalse} @@ -333,5 +348,6 @@ Scripting.addGlobal(function lookupPresBoxField(container: Doc, field: string, d if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 50 : 46; if (field === 'presStatus') return container.presStatus; if (field === '_itemIndex') return container._itemIndex; + if (field === 'presBox') return container; return undefined; }); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 5d4af2d77..29e3c008a 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -6,7 +6,7 @@ import { action, computed, IReactionDisposer, observable, runInAction } from "mo import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { documentSchema } from "../../../fields/documentSchemas"; -import { makeInterface } from "../../../fields/Schema"; +import { makeInterface, listSpec } from "../../../fields/Schema"; import { Cast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils"; @@ -18,6 +18,8 @@ import { ViewBoxBaseComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; +import { Doc, WidthSym, HeightSym } from "../../../fields/Doc"; +import { OverlayView } from "../OverlayView"; const path = require('path'); type ScreenshotDocument = makeInterface<[typeof documentSchema]>; @@ -72,7 +74,14 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _width: 150, _height: height / width * 150, title: "--screenshot--" }); - this.props.addDocument?.(imageSummary); + 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().myOverlayDocuments, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); + } else { + this.props.addDocument?.(imageSummary); + } } }, 500); }); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index ccf1f5588..9d02239fc 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,6 +1,4 @@ import React = require("react"); -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faVideo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; @@ -8,7 +6,6 @@ import * as rp from 'request-promise'; import { Doc } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; import { createSchema, makeInterface } from "../../../fields/Schema"; -import { ScriptField } from "../../../fields/ScriptField"; import { Cast, StrCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; import { Utils, emptyFunction, returnOne, returnZero } from "../../../Utils"; @@ -22,6 +19,7 @@ import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; import { documentSchema } from "../../../fields/documentSchemas"; +import { Networking } from "../../Network"; const path = require('path'); export const timeSchema = createSchema({ @@ -30,8 +28,6 @@ export const timeSchema = createSchema({ type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>; const VideoDocument = makeInterface(documentSchema, timeSchema); -library.add(faVideo); - @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { static _youtubeIframeCounter: number = 0; @@ -104,41 +100,59 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD canvas.height = 640 * (this.layoutDoc._nativeHeight || 0) / (this.layoutDoc._nativeWidth || 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(); + // 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) { // can't find a way to take snapshots of videos - const b = Docs.Create.ButtonDocument({ + if (!this._videoRef) { + 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() + _width: 150, _height: 50, title: (this.layoutDoc.currentTimecode || 0).toString(), + }); + b.isLinkButton = true; + this.props.addDocument?.(b); + DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); + Networking.PostToServer("/youtubeScreenshot", { + id: this.youtubeVideoId, + timecode: this.layoutDoc.currentTimecode + }).then(response => { + const resolved = response?.accessPaths?.agnostic?.client; + if (resolved) { + this.props.removeDocument?.(b); + this.createRealSummaryLink(resolved); + } }); - b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.layoutDoc.currentTimecode || 0)}`); } else { //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("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc.currentTimecode || 0).toString().replace(/\./, "_"))); - VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => { if (returnedFilename) { - const url = this.choosePath(Utils.prepend(returnedFilename)); - const imageSummary = Docs.Create.ImageDocument(url, { - _nativeWidth: this.layoutDoc._nativeWidth, _nativeHeight: this.layoutDoc._nativeHeight, - x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), - _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc.currentTimecode || 0) + " image-" - }); - Doc.GetProto(imageSummary)["data-nativeWidth"] = this.layoutDoc._nativeWidth; - Doc.GetProto(imageSummary)["data-nativeHeight"] = this.layoutDoc._nativeHeight; - imageSummary.isLinkButton = true; - this.props.addDocument && this.props.addDocument(imageSummary); - DocUtils.MakeLink({ doc: imageSummary }, { doc: this.rootDoc }, "video snapshot"); + this.createRealSummaryLink(returnedFilename); } }); } } + private createRealSummaryLink = (relative: string) => { + const url = this.choosePath(Utils.prepend(relative)); + const width = (this.layoutDoc._width || 0); + const height = (this.layoutDoc._height || 0); + const imageSummary = Docs.Create.ImageDocument(url, { + _nativeWidth: this.layoutDoc._nativeWidth, _nativeHeight: this.layoutDoc._nativeHeight, + x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), + _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc.currentTimecode || 0) + " image-" + }); + Doc.GetProto(imageSummary)["data-nativeWidth"] = this.layoutDoc._nativeWidth; + Doc.GetProto(imageSummary)["data-nativeHeight"] = this.layoutDoc._nativeHeight; + imageSummary.isLinkButton = true; + this.props.addDocument?.(imageSummary); + DocUtils.MakeLink({ doc: imageSummary }, { doc: this.rootDoc }, "video snapshot"); + } + @action updateTimecode = () => { this.player && (this.layoutDoc.currentTimecode = this.player.currentTime); @@ -162,8 +176,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD componentWillUnmount() { this.Pause(); - this._reactionDisposer && this._reactionDisposer(); - this._youtubeReactionDisposer && this._youtubeReactionDisposer(); + this._reactionDisposer?.(); + this._youtubeReactionDisposer?.(); } @action diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 82f05012a..a91d4dfd9 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,12 +1,12 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faStickyNote, faPen, faMousePointer } from '@fortawesome/free-solid-svg-icons'; -import { action, computed, observable, trace, IReactionDisposer, reaction } from "mobx"; +import { action, computed, observable, trace, IReactionDisposer, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, FieldResult } from "../../../fields/Doc"; +import { Doc, FieldResult, DocListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { HtmlField } from "../../../fields/HtmlField"; import { InkTool } from "../../../fields/InkField"; -import { makeInterface } from "../../../fields/Schema"; +import { makeInterface, listSpec } from "../../../fields/Schema"; import { Cast, NumCast, BoolCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { Utils, returnOne, emptyFunction, returnZero } from "../../../Utils"; @@ -22,6 +22,10 @@ import React = require("react"); import * as WebRequest from 'web-request'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { undoBatch } from "../../util/UndoManager"; +import { List } from "../../../fields/List"; const htmlToText = require("html-to-text"); library.add(faStickyNote); @@ -33,7 +37,7 @@ const WebDocument = makeInterface(documentSchema); export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } - get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) === "disabled"; } + get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; } set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; } @observable private _url: string = "hello"; @observable private _pressX: number = 0; @@ -48,19 +52,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); iframeLoaded = action((e: any) => { - if (this._iframeRef.current?.contentDocument) { - this._iframeRef.current.contentDocument.addEventListener('pointerdown', this.iframedown, false); - this._iframeRef.current.contentDocument.addEventListener('scroll', this.iframeScrolled, false); - this.layoutDoc.scrollHeight = this._iframeRef.current.contentDocument.children?.[0].scrollHeight || 1000; - this._iframeRef.current.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop); + const iframe = this._iframeRef.current; + if (iframe && iframe.contentDocument) { + iframe.setAttribute("enable-annotation", "true"); + iframe.contentDocument.addEventListener('pointerdown', this.iframedown, false); + iframe.contentDocument.addEventListener('scroll', this.iframeScrolled, false); + this.layoutDoc.scrollHeight = iframe.contentDocument.children?.[0].scrollHeight || 1000; + iframe.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop); + iframe.contentDocument.children[0].scrollLeft = NumCast(this.layoutDoc.scrollLeft); } this._reactionDisposer?.(); - this._reactionDisposer = reaction(() => this.layoutDoc.scrollY, - (scrollY) => { - if (scrollY !== undefined) { - this._outerRef.current!.scrollTop = scrollY; + this._reactionDisposer = reaction(() => ({ y: this.layoutDoc.scrollY, x: this.layoutDoc.scrollX }), + ({ x, y }) => { + if (y !== undefined) { + this._outerRef.current!.scrollTop = y; this.layoutDoc.scrollY = undefined; } + if (x !== undefined) { + this._outerRef.current!.scrollLeft = x; + this.layoutDoc.scrollX = undefined; + } }, { fireImmediately: true } ); @@ -70,14 +81,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._setPreviewCursor?.(e.screenX, e.screenY, false); } iframeScrolled = (e: any) => { - const scroll = e.target?.children?.[0].scrollTop; - this.layoutDoc.scrollTop = this._outerRef.current!.scrollTop = scroll; + const scrollTop = e.target?.children?.[0].scrollTop; + const scrollLeft = e.target?.children?.[0].scrollLeft; + this.layoutDoc.scrollTop = this._outerRef.current!.scrollTop = scrollTop; + this.layoutDoc.scrollLeft = this._outerRef.current!.scrollLeft = scrollLeft; } async componentDidMount() { - - this.setURL(); - - this._iframeRef.current!.setAttribute("enable-annotation", "true"); + const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); + runInAction(() => this._url = urlField?.url.toString() || ""); document.addEventListener("pointerup", this.onLongPressUp); document.addEventListener("pointermove", this.onLongPressMove); @@ -86,14 +97,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const youtubeaspect = 400 / 315; const nativeWidth = NumCast(this.layoutDoc._nativeWidth); const nativeHeight = NumCast(this.layoutDoc._nativeHeight); - if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { - if (!nativeWidth) this.layoutDoc._nativeWidth = 600; - this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect; - this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; - } + if (field) { + if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { + if (!nativeWidth) this.layoutDoc._nativeWidth = 600; + this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect; + this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; + } + } // else it's an HTMLfield } else if (field?.url) { const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); - this.dataDoc.text = htmlToText.fromString(result.content); + if (result) { + this.dataDoc.text = htmlToText.fromString(result.content); + } } } @@ -101,8 +116,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._reactionDisposer?.(); document.removeEventListener("pointerup", this.onLongPressUp); document.removeEventListener("pointermove", this.onLongPressMove); - this._iframeRef.current!.contentDocument?.removeEventListener('pointerdown', this.iframedown); - this._iframeRef.current!.contentDocument?.removeEventListener('scroll', this.iframeScrolled); + this._iframeRef.current?.contentDocument?.removeEventListener('pointerdown', this.iframedown); + this._iframeRef.current?.contentDocument?.removeEventListener('scroll', this.iframeScrolled); } @action @@ -110,16 +125,73 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._url = e.target.value; } + onUrlDragover = (e: React.DragEvent) => { + e.preventDefault(); + } @action - submitURL = () => { - this.dataDoc[this.props.fieldKey] = new WebField(new URL(this._url)); + onUrlDrop = (e: React.DragEvent) => { + const { dataTransfer } = e; + const html = dataTransfer.getData("text/html"); + const uri = dataTransfer.getData("text/uri-list"); + const url = uri || html || this._url; + this._url = url.startsWith(window.location.origin) ? + url.replace(window.location.origin, this._url.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; + this.submitURL(); + e.stopPropagation(); } @action - setURL() { - const urlField: FieldResult<WebField> = Cast(this.dataDoc[this.props.fieldKey], WebField); - if (urlField) this._url = urlField.url.toString(); - else this._url = ""; + forward = () => { + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); + if (future.length) { + history.push(this._url); + this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey])); + this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!)); + this.dataDoc[this.annotationKey] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)])); + } + } + + @action + back = () => { + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); + if (history.length) { + if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); + else future.push(this._url); + this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey])); + this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!)); + this.dataDoc[this.annotationKey] = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey + "-" + this.urlHash(this._url)])); + } + } + + urlHash(s: string) { + return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); + } + @action + submitURL = () => { + if (!this._url.startsWith("http")) this._url = "http://" + this._url; + try { + const URLy = new URL(this._url); + const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), null); + const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), null); + const annos = DocListCast(this.dataDoc[this.annotationKey]); + const url = Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.toString(); + if (url) { + if (history === undefined) { + this.dataDoc[this.fieldKey + "-history"] = new List<string>([url]); + + } else { + history.push(url); + } + future && (future.length = 0); + this.dataDoc[this.annotationKey + "-" + this.urlHash(url)] = new List<Doc>(annos); + } + this.dataDoc[this.fieldKey] = new WebField(URLy); + this.dataDoc[this.annotationKey] = new List<Doc>([]); + } catch (e) { + console.log("Error in URL :" + this._url); + } } onValueKeyDown = async (e: React.KeyboardEvent) => { @@ -130,19 +202,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } toggleAnnotationMode = () => { - if (!this.layoutDoc.isAnnotating) { - this.layoutDoc.lockedTransform = false; - this.layoutDoc.isAnnotating = true; - } - else { - this.layoutDoc.lockedTransform = true; - this.layoutDoc.isAnnotating = false; - } + this.layoutDoc.isAnnotating = !this.layoutDoc.isAnnotating; } urlEditor() { return ( - <div className="webBox-urlEditor" style={{ top: this._collapsed ? -70 : 0 }}> + <div className="webBox-urlEditor" + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} style={{ top: this._collapsed ? -70 : 0 }}> <div className="urlEditor"> <div className="editorBase"> <button className="editor-collapse" @@ -155,7 +222,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum title="Collapse Url Editor" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> </button> - <div className="webBox-buttons" style={{ display: this._collapsed ? "none" : "flex" }}> + <div className="webBox-buttons" + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} style={{ display: this._collapsed ? "none" : "flex" }}> <div className="webBox-freeze" title={"Annotate"} style={{ background: this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} > <FontAwesomeIcon icon={faPen} size={"2x"} /> </div> @@ -165,6 +234,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <input className="webpage-urlInput" placeholder="ENTER URL" value={this._url} + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} onChange={this.onURLChange} onKeyDown={this.onValueKeyDown} /> @@ -172,10 +243,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum display: "flex", flexDirection: "row", justifyContent: "space-between", - minWidth: "100px", + maxWidth: "120px", }}> - <button className="submitUrl" onClick={this.submitURL}> - SUBMIT + <button className="submitUrl" onClick={this.submitURL} + onDragOver={this.onUrlDragover} onDrop={this.onUrlDrop}> + 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> </div> </div> @@ -308,6 +386,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } + + @undoBatch + @action + toggleNativeDimensions = () => { + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); + } + specificContextMenu = (e: React.MouseEvent): void => { + const cm = ContextMenu.Instance; + const funcs: ContextMenuProps[] = []; + funcs.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); + cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + + } + //const href = "https://brown365-my.sharepoint.com/personal/bcz_ad_brown_edu/_layouts/15/Doc.aspx?sourcedoc={31aa3178-4c21-4474-b367-877d0a7135e4}&action=embedview&wdStartOn=1"; @computed @@ -348,7 +440,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum {this.urlEditor()} </>); } - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.Document.scrollTop)); + scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc.scrollLeft), NumCast(this.layoutDoc.scrollTop)); render() { return (<div className={`webBox-container`} style={{ @@ -356,18 +448,27 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%", height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%", pointerEvents: this.layoutDoc.isBackground ? "none" : undefined - }} > + }} + onContextMenu={this.specificContextMenu}> + <base target="_blank" /> {this.content} <div className={"webBox-outerContent"} ref={this._outerRef} style={{ pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none" }} onWheel={e => e.stopPropagation()} onScroll={e => { - if (this._iframeRef.current!.contentDocument!.children[0].scrollTop !== this._outerRef.current!.scrollTop) { - this._iframeRef.current!.contentDocument!.children[0].scrollTop = this._outerRef.current!.scrollTop; + const iframe = this._iframeRef?.current?.contentDocument; + const outerFrame = this._outerRef.current; + if (iframe && outerFrame) { + if (iframe.children[0].scrollTop !== outerFrame.scrollTop) { + iframe.children[0].scrollTop = outerFrame.scrollTop; + } + if (iframe.children[0].scrollLeft !== outerFrame.scrollLeft) { + iframe.children[0].scrollLeft = outerFrame.scrollLeft; + } } //this._outerRef.current!.scrollTop !== this._scrollTop && (this._outerRef.current!.scrollTop = this._scrollTop) }}> - <div className={"webBox-innerContent"} style={{ height: NumCast(this.layoutDoc.scrollHeight) }}> + <div className={"webBox-innerContent"} style={{ height: NumCast(this.layoutDoc.scrollHeight), width: 4000 }}> <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 5e33e7e70..fc131cd38 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -206,6 +206,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (json !== curLayout?.Data) { !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); + !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily)); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited } @@ -868,7 +869,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); - const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true }); + const link = view.state.schema.mark(view.state.schema.marks.link, { href: Utils.prepend(`/doc/${linkId}`), location: "onRight", title: title, docref: true }); marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } @@ -1178,7 +1179,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @action tryUpdateHeight(limitHeight?: number) { let scrollHeight = this._ref.current?.scrollHeight; - if (this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + if (this.props.renderDepth && this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation scrollHeight = scrollHeight * NumCast(this.layoutDoc.scale, 1); if (limitHeight && scrollHeight > limitHeight) { scrollHeight = limitHeight; @@ -1223,14 +1224,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp transform: `scale(${scale})`, transformOrigin: "top left", width: `${100 / scale}%`, - height: `${100 / scale}%`, + height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`, ...this.styleFromLayoutString(scale) }}> <div className={`formattedTextBox-cont`} ref={this._ref} style={{ width: "100%", - height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`, - background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), + height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined, + background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"), pointerEvents: interactive ? "none" : undefined, diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 2f7d23021..0e3e7f91e 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -154,15 +154,12 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const originalDoc = layoutDoc.rootDocument || layoutDoc; if (originalDoc instanceof Doc) { const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Docs.Create.TextDocument("", { - layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, - layoutKey, - _singleLine: BoolCast(originalDoc._singleLine), - x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) - }); + const newDoc = Doc.MakeCopy(originalDoc, true); + newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; } + Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); } @@ -178,15 +175,12 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const originalDoc = layoutDoc.rootDocument || layoutDoc; if (force || props.Document._singleLine) { const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Docs.Create.TextDocument("", { - layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, - layoutKey, - _singleLine: BoolCast(originalDoc._singleLine), - x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) - }); + const newDoc = Doc.MakeCopy(originalDoc, true); + newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; } + Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); return true; |